From c89377d12fdfb243daee82990faf420171620076 Mon Sep 17 00:00:00 2001 From: dahoud Date: Sun, 9 Nov 2025 17:06:37 +0000 Subject: [PATCH] feat: Module server-impl-quarkus initial MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Module d'implémentation serveur pour lions-user-manager Contenu: - KeycloakAdminClient avec résilience (Circuit Breaker, Retry, Timeout) - UserServiceImpl (25+ méthodes) - RoleServiceImpl (20+ méthodes) - AuditServiceImpl (logging et statistiques) - UserResource, RoleResource (REST API) - Mappers (User, Role) - Health checks - Configurations dev/prod séparées Statut: 🔄 80% complété 🤖 Generated with Claude Code Co-Authored-By: Claude --- pom.xml | 206 ++++++ .../manager/client/KeycloakAdminClient.java | 63 ++ .../client/KeycloakAdminClientImpl.java | 163 +++++ .../lions/user/manager/mapper/RoleMapper.java | 76 +++ .../lions/user/manager/mapper/UserMapper.java | 173 +++++ .../resource/HealthResourceEndpoint.java | 72 +++ .../manager/resource/KeycloakHealthCheck.java | 50 ++ .../user/manager/resource/RoleResource.java | 509 +++++++++++++++ .../user/manager/resource/UserResource.java | 406 ++++++++++++ .../service/impl/AuditServiceImpl.java | 371 +++++++++++ .../manager/service/impl/RoleServiceImpl.java | 609 ++++++++++++++++++ .../manager/service/impl/UserServiceImpl.java | 472 ++++++++++++++ src/main/resources/application-dev.properties | 82 +++ .../resources/application-prod.properties | 113 ++++ src/main/resources/application.properties | 100 +++ target/classes/application-dev.properties | 82 +++ target/classes/application-prod.properties | 113 ++++ target/classes/application.properties | 100 +++ .../manager/client/KeycloakAdminClient.class | Bin 0 -> 604 bytes .../client/KeycloakAdminClientImpl.class | Bin 0 -> 6372 bytes .../user/manager/mapper/RoleMapper.class | Bin 0 -> 3632 bytes .../user/manager/mapper/UserMapper.class | Bin 0 -> 7456 bytes .../resource/HealthResourceEndpoint.class | Bin 0 -> 3525 bytes .../resource/KeycloakHealthCheck.class | Bin 0 -> 2232 bytes .../resource/RoleResource$ErrorResponse.class | Bin 0 -> 742 bytes .../RoleResource$RoleAssignmentRequest.class | Bin 0 -> 824 bytes .../user/manager/resource/RoleResource.class | Bin 0 -> 13914 bytes .../resource/UserResource$ErrorResponse.class | Bin 0 -> 742 bytes .../UserResource$PasswordResetRequest.class | Bin 0 -> 895 bytes ...UserResource$SessionsRevokedResponse.class | Bin 0 -> 768 bytes .../user/manager/resource/UserResource.class | Bin 0 -> 14966 bytes .../service/impl/AuditServiceImpl.class | Bin 0 -> 12566 bytes .../service/impl/RoleServiceImpl.class | Bin 0 -> 17322 bytes .../service/impl/UserServiceImpl.class | Bin 0 -> 20940 bytes ...user-manager-server-impl-quarkus-1.0.0.jar | Bin 0 -> 32935 bytes target/maven-archiver/pom.properties | 3 + .../compile/default-compile/createdFiles.lst | 10 + .../compile/default-compile/inputFiles.lst | 7 + 38 files changed, 3780 insertions(+) create mode 100644 pom.xml create mode 100644 src/main/java/dev/lions/user/manager/client/KeycloakAdminClient.java create mode 100644 src/main/java/dev/lions/user/manager/client/KeycloakAdminClientImpl.java create mode 100644 src/main/java/dev/lions/user/manager/mapper/RoleMapper.java create mode 100644 src/main/java/dev/lions/user/manager/mapper/UserMapper.java create mode 100644 src/main/java/dev/lions/user/manager/resource/HealthResourceEndpoint.java create mode 100644 src/main/java/dev/lions/user/manager/resource/KeycloakHealthCheck.java create mode 100644 src/main/java/dev/lions/user/manager/resource/RoleResource.java create mode 100644 src/main/java/dev/lions/user/manager/resource/UserResource.java create mode 100644 src/main/java/dev/lions/user/manager/service/impl/AuditServiceImpl.java create mode 100644 src/main/java/dev/lions/user/manager/service/impl/RoleServiceImpl.java create mode 100644 src/main/java/dev/lions/user/manager/service/impl/UserServiceImpl.java create mode 100644 src/main/resources/application-dev.properties create mode 100644 src/main/resources/application-prod.properties create mode 100644 src/main/resources/application.properties create mode 100644 target/classes/application-dev.properties create mode 100644 target/classes/application-prod.properties create mode 100644 target/classes/application.properties create mode 100644 target/classes/dev/lions/user/manager/client/KeycloakAdminClient.class create mode 100644 target/classes/dev/lions/user/manager/client/KeycloakAdminClientImpl.class create mode 100644 target/classes/dev/lions/user/manager/mapper/RoleMapper.class create mode 100644 target/classes/dev/lions/user/manager/mapper/UserMapper.class create mode 100644 target/classes/dev/lions/user/manager/resource/HealthResourceEndpoint.class create mode 100644 target/classes/dev/lions/user/manager/resource/KeycloakHealthCheck.class create mode 100644 target/classes/dev/lions/user/manager/resource/RoleResource$ErrorResponse.class create mode 100644 target/classes/dev/lions/user/manager/resource/RoleResource$RoleAssignmentRequest.class create mode 100644 target/classes/dev/lions/user/manager/resource/RoleResource.class create mode 100644 target/classes/dev/lions/user/manager/resource/UserResource$ErrorResponse.class create mode 100644 target/classes/dev/lions/user/manager/resource/UserResource$PasswordResetRequest.class create mode 100644 target/classes/dev/lions/user/manager/resource/UserResource$SessionsRevokedResponse.class create mode 100644 target/classes/dev/lions/user/manager/resource/UserResource.class create mode 100644 target/classes/dev/lions/user/manager/service/impl/AuditServiceImpl.class create mode 100644 target/classes/dev/lions/user/manager/service/impl/RoleServiceImpl.class create mode 100644 target/classes/dev/lions/user/manager/service/impl/UserServiceImpl.class create mode 100644 target/lions-user-manager-server-impl-quarkus-1.0.0.jar create mode 100644 target/maven-archiver/pom.properties create mode 100644 target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst create mode 100644 target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..cb22eeb --- /dev/null +++ b/pom.xml @@ -0,0 +1,206 @@ + + + 4.0.0 + + + dev.lions.user.manager + lions-user-manager-parent + 1.0.0 + + + lions-user-manager-server-impl-quarkus + jar + + Lions User Manager - Server Implementation (Quarkus) + Implémentation serveur: Resources REST, Services, Keycloak Admin Client + + + + + dev.lions.user.manager + lions-user-manager-server-api + + + + + io.quarkus + quarkus-rest + + + + io.quarkus + quarkus-rest-jackson + + + + io.quarkus + quarkus-oidc + + + + io.quarkus + quarkus-security + + + + io.quarkus + quarkus-smallrye-openapi + + + + io.quarkus + quarkus-smallrye-health + + + + io.quarkus + quarkus-micrometer-registry-prometheus + + + + io.quarkus + quarkus-hibernate-validator + + + + io.quarkus + quarkus-rest-client-jackson + + + + io.quarkus + quarkus-smallrye-fault-tolerance + + + + + org.keycloak + keycloak-admin-client + 23.0.3 + + + org.jboss.resteasy + resteasy-client + + + org.jboss.resteasy + resteasy-multipart-provider + + + org.jboss.resteasy + resteasy-jackson2-provider + + + + + + + io.quarkus + quarkus-hibernate-orm-panache + true + + + + io.quarkus + quarkus-jdbc-postgresql + true + + + + io.quarkus + quarkus-flyway + true + + + + + org.projectlombok + lombok + + + + + org.mapstruct + mapstruct + + + + + io.quarkus + quarkus-junit5 + test + + + + io.rest-assured + rest-assured + test + + + + org.testcontainers + junit-jupiter + test + + + + org.testcontainers + postgresql + test + + + + org.mockito + mockito-core + test + + + + + + + io.quarkus.platform + quarkus-maven-plugin + + + + build + generate-code + generate-code-tests + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + org.apache.maven.plugins + maven-surefire-plugin + + + + org.apache.maven.plugins + maven-failsafe-plugin + + + + integration-test + verify + + + + + + + org.jacoco + jacoco-maven-plugin + + + + diff --git a/src/main/java/dev/lions/user/manager/client/KeycloakAdminClient.java b/src/main/java/dev/lions/user/manager/client/KeycloakAdminClient.java new file mode 100644 index 0000000..75693f4 --- /dev/null +++ b/src/main/java/dev/lions/user/manager/client/KeycloakAdminClient.java @@ -0,0 +1,63 @@ +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); + + /** + * Ferme la connexion Keycloak + */ + void close(); + + /** + * Force la reconnexion + */ + void reconnect(); +} diff --git a/src/main/java/dev/lions/user/manager/client/KeycloakAdminClientImpl.java b/src/main/java/dev/lions/user/manager/client/KeycloakAdminClientImpl.java new file mode 100644 index 0000000..cf581b9 --- /dev/null +++ b/src/main/java/dev/lions/user/manager/client/KeycloakAdminClientImpl.java @@ -0,0 +1,163 @@ +package dev.lions.user.manager.client; + +import io.quarkus.runtime.Startup; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import jakarta.enterprise.context.ApplicationScoped; +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.KeycloakBuilder; +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.time.temporal.ChronoUnit; + +/** + * Implémentation du client Keycloak Admin + * Utilise Circuit Breaker, Retry et Timeout pour la résilience + */ +@ApplicationScoped +@Startup +@Slf4j +public class KeycloakAdminClientImpl implements KeycloakAdminClient { + + @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; + + @ConfigProperty(name = "lions.keycloak.admin-password") + String adminPassword; + + @ConfigProperty(name = "lions.keycloak.connection-pool-size", defaultValue = "10") + Integer connectionPoolSize; + + @ConfigProperty(name = "lions.keycloak.timeout-seconds", defaultValue = "30") + Integer timeoutSeconds; + + private Keycloak keycloak; + + @PostConstruct + void init() { + log.info("Initialisation du client Keycloak Admin..."); + log.info("Server URL: {}", serverUrl); + log.info("Admin Realm: {}", adminRealm); + log.info("Admin Client ID: {}", adminClientId); + log.info("Admin Username: {}", adminUsername); + + try { + this.keycloak = KeycloakBuilder.builder() + .serverUrl(serverUrl) + .realm(adminRealm) + .clientId(adminClientId) + .username(adminUsername) + .password(adminPassword) + .build(); + + // Test de connexion + keycloak.serverInfo().getInfo(); + log.info("✅ Connexion à Keycloak réussie!"); + } catch (Exception e) { + log.error("❌ Échec de la connexion à Keycloak: {}", e.getMessage(), e); + throw new RuntimeException("Impossible de se connecter à Keycloak", 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 Keycloak getInstance() { + if (keycloak == null) { + log.warn("Instance Keycloak null, tentative de réinitialisation..."); + init(); + } + 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 getInstance().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 { + if (keycloak == null) { + return false; + } + keycloak.serverInfo().getInfo(); + return true; + } catch (Exception e) { + log.warn("Keycloak non connecté: {}", e.getMessage()); + return false; + } + } + + @Override + public boolean realmExists(String realmName) { + try { + getRealm(realmName).toRepresentation(); + return true; + } catch (NotFoundException e) { + return false; + } catch (Exception e) { + log.error("Erreur lors de la vérification de l'existence du realm {}: {}", realmName, e.getMessage()); + return false; + } + } + + @PreDestroy + @Override + public void close() { + if (keycloak != null) { + log.info("Fermeture de la connexion Keycloak..."); + keycloak.close(); + keycloak = null; + } + } + + @Override + public void reconnect() { + log.info("Reconnexion à Keycloak..."); + close(); + init(); + } +} diff --git a/src/main/java/dev/lions/user/manager/mapper/RoleMapper.java b/src/main/java/dev/lions/user/manager/mapper/RoleMapper.java new file mode 100644 index 0000000..f70e04b --- /dev/null +++ b/src/main/java/dev/lions/user/manager/mapper/RoleMapper.java @@ -0,0 +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()) + .nom(roleRep.getName()) + .description(roleRep.getDescription()) + .typeRole(typeRole) + .realmName(realmName) + .composite(roleRep.isComposite() != null ? roleRep.isComposite() : false) + .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.getNom()); + 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 toDTOList(List 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 toRepresentationList(List roleDTOs) { + if (roleDTOs == null) { + return List.of(); + } + + return roleDTOs.stream() + .map(RoleMapper::toRepresentation) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/dev/lions/user/manager/mapper/UserMapper.java b/src/main/java/dev/lions/user/manager/mapper/UserMapper.java new file mode 100644 index 0000000..a09b734 --- /dev/null +++ b/src/main/java/dev/lions/user/manager/mapper/UserMapper.java @@ -0,0 +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> 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 toDTOList(List 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 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() + ); + } +} diff --git a/src/main/java/dev/lions/user/manager/resource/HealthResourceEndpoint.java b/src/main/java/dev/lions/user/manager/resource/HealthResourceEndpoint.java new file mode 100644 index 0000000..21bf146 --- /dev/null +++ b/src/main/java/dev/lions/user/manager/resource/HealthResourceEndpoint.java @@ -0,0 +1,72 @@ +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 getKeycloakHealth() { + Map health = new HashMap<>(); + + try { + boolean connected = keycloakAdminClient.isConnected(); + health.put("status", connected ? "UP" : "DOWN"); + health.put("connected", connected); + health.put("timestamp", System.currentTimeMillis()); + + if (connected) { + // Récupérer info serveur Keycloak + var serverInfo = keycloakAdminClient.getInstance().serverInfo().getInfo(); + health.put("keycloakVersion", serverInfo.getSystemInfo().getVersion()); + } + } 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 getServiceStatus() { + Map 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; + } +} diff --git a/src/main/java/dev/lions/user/manager/resource/KeycloakHealthCheck.java b/src/main/java/dev/lions/user/manager/resource/KeycloakHealthCheck.java new file mode 100644 index 0000000..a78ef63 --- /dev/null +++ b/src/main/java/dev/lions/user/manager/resource/KeycloakHealthCheck.java @@ -0,0 +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(); + } + } +} diff --git a/src/main/java/dev/lions/user/manager/resource/RoleResource.java b/src/main/java/dev/lions/user/manager/resource/RoleResource.java new file mode 100644 index 0000000..826ec1d --- /dev/null +++ b/src/main/java/dev/lions/user/manager/resource/RoleResource.java @@ -0,0 +1,509 @@ +package dev.lions.user.manager.resource; + +import dev.lions.user.manager.dto.role.RoleDTO; +import dev.lions.user.manager.service.RoleService; +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.*; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.media.Content; +import org.eclipse.microprofile.openapi.annotations.media.Schema; +import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponses; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; + +import java.util.List; + +/** + * REST Resource pour la gestion des rôles Keycloak + * Endpoints pour les rôles realm, rôles client, et attributions + */ +@Path("/api/roles") +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +@Tag(name = "Roles", description = "Gestion des rôles Keycloak (realm et client)") +@Slf4j +public class RoleResource { + + @Inject + RoleService roleService; + + // ==================== Endpoints Realm Roles ==================== + + @POST + @Path("/realm") + @Operation(summary = "Créer un rôle realm", description = "Crée un nouveau rôle au niveau du realm") + @APIResponses({ + @APIResponse(responseCode = "201", description = "Rôle créé", + content = @Content(schema = @Schema(implementation = RoleDTO.class))), + @APIResponse(responseCode = "400", description = "Données invalides"), + @APIResponse(responseCode = "409", description = "Rôle existe déjà"), + @APIResponse(responseCode = "500", description = "Erreur serveur") + }) + @RolesAllowed({"admin", "role_manager"}) + public Response createRealmRole( + @Valid @NotNull RoleDTO roleDTO, + @QueryParam("realm") @NotBlank String realmName + ) { + log.info("POST /api/roles/realm - Création du rôle realm: {} dans le realm: {}", + roleDTO.getNom(), 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 ErrorResponse(e.getMessage())) + .build(); + } catch (Exception e) { + log.error("Erreur lors de la création du rôle realm", e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(new ErrorResponse(e.getMessage())) + .build(); + } + } + + @GET + @Path("/realm/{roleName}") + @Operation(summary = "Récupérer un rôle realm par nom", description = "Récupère les détails d'un rôle realm") + @APIResponses({ + @APIResponse(responseCode = "200", description = "Rôle trouvé", + content = @Content(schema = @Schema(implementation = RoleDTO.class))), + @APIResponse(responseCode = "404", description = "Rôle non trouvé"), + @APIResponse(responseCode = "500", description = "Erreur serveur") + }) + @RolesAllowed({"admin", "role_manager", "role_viewer"}) + public Response getRealmRole( + @Parameter(description = "Nom du rôle") @PathParam("roleName") @NotBlank String roleName, + @Parameter(description = "Nom du realm") @QueryParam("realm") @NotBlank String realmName + ) { + log.info("GET /api/roles/realm/{} - realm: {}", roleName, realmName); + + try { + return roleService.getRealmRoleByName(roleName, realmName) + .map(role -> Response.ok(role).build()) + .orElse(Response.status(Response.Status.NOT_FOUND) + .entity(new ErrorResponse("Rôle non trouvé")) + .build()); + } catch (Exception e) { + log.error("Erreur lors de la récupération du rôle realm {}", roleName, e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(new ErrorResponse(e.getMessage())) + .build(); + } + } + + @GET + @Path("/realm") + @Operation(summary = "Lister tous les rôles realm", description = "Liste tous les rôles du realm") + @APIResponses({ + @APIResponse(responseCode = "200", description = "Liste des rôles"), + @APIResponse(responseCode = "500", description = "Erreur serveur") + }) + @RolesAllowed({"admin", "role_manager", "role_viewer"}) + public Response getAllRealmRoles( + @QueryParam("realm") @NotBlank String realmName + ) { + log.info("GET /api/roles/realm - realm: {}", realmName); + + try { + List roles = roleService.getAllRealmRoles(realmName); + return Response.ok(roles).build(); + } catch (Exception e) { + log.error("Erreur lors de la récupération des rôles realm", e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(new ErrorResponse(e.getMessage())) + .build(); + } + } + + @PUT + @Path("/realm/{roleName}") + @Operation(summary = "Mettre à jour un rôle realm", description = "Met à jour les informations d'un rôle realm") + @APIResponses({ + @APIResponse(responseCode = "200", description = "Rôle mis à jour", + content = @Content(schema = @Schema(implementation = RoleDTO.class))), + @APIResponse(responseCode = "404", description = "Rôle non trouvé"), + @APIResponse(responseCode = "500", description = "Erreur serveur") + }) + @RolesAllowed({"admin", "role_manager"}) + public Response updateRealmRole( + @PathParam("roleName") @NotBlank String roleName, + @Valid @NotNull RoleDTO roleDTO, + @QueryParam("realm") @NotBlank String realmName + ) { + log.info("PUT /api/roles/realm/{} - realm: {}", roleName, realmName); + + try { + RoleDTO updatedRole = roleService.updateRealmRole(roleName, roleDTO, realmName); + return Response.ok(updatedRole).build(); + } catch (Exception e) { + log.error("Erreur lors de la mise à jour du rôle realm {}", roleName, e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(new ErrorResponse(e.getMessage())) + .build(); + } + } + + @DELETE + @Path("/realm/{roleName}") + @Operation(summary = "Supprimer un rôle realm", description = "Supprime un rôle realm") + @APIResponses({ + @APIResponse(responseCode = "204", description = "Rôle supprimé"), + @APIResponse(responseCode = "404", description = "Rôle non trouvé"), + @APIResponse(responseCode = "500", description = "Erreur serveur") + }) + @RolesAllowed({"admin"}) + public Response deleteRealmRole( + @PathParam("roleName") @NotBlank String roleName, + @QueryParam("realm") @NotBlank String realmName + ) { + log.info("DELETE /api/roles/realm/{} - realm: {}", roleName, realmName); + + try { + roleService.deleteRealmRole(roleName, realmName); + return Response.noContent().build(); + } catch (Exception e) { + log.error("Erreur lors de la suppression du rôle realm {}", roleName, e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(new ErrorResponse(e.getMessage())) + .build(); + } + } + + // ==================== Endpoints Client Roles ==================== + + @POST + @Path("/client/{clientId}") + @Operation(summary = "Créer un rôle client", description = "Crée un nouveau rôle pour un client spécifique") + @APIResponses({ + @APIResponse(responseCode = "201", description = "Rôle créé", + content = @Content(schema = @Schema(implementation = RoleDTO.class))), + @APIResponse(responseCode = "400", description = "Données invalides"), + @APIResponse(responseCode = "409", description = "Rôle existe déjà"), + @APIResponse(responseCode = "500", description = "Erreur serveur") + }) + @RolesAllowed({"admin", "role_manager"}) + public Response createClientRole( + @PathParam("clientId") @NotBlank String clientId, + @Valid @NotNull RoleDTO roleDTO, + @QueryParam("realm") @NotBlank 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 ErrorResponse(e.getMessage())) + .build(); + } catch (Exception e) { + log.error("Erreur lors de la création du rôle client", e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(new ErrorResponse(e.getMessage())) + .build(); + } + } + + @GET + @Path("/client/{clientId}/{roleName}") + @Operation(summary = "Récupérer un rôle client par nom", description = "Récupère les détails d'un rôle client") + @APIResponses({ + @APIResponse(responseCode = "200", description = "Rôle trouvé", + content = @Content(schema = @Schema(implementation = RoleDTO.class))), + @APIResponse(responseCode = "404", description = "Rôle non trouvé"), + @APIResponse(responseCode = "500", description = "Erreur serveur") + }) + @RolesAllowed({"admin", "role_manager", "role_viewer"}) + public Response getClientRole( + @PathParam("clientId") @NotBlank String clientId, + @PathParam("roleName") @NotBlank String roleName, + @QueryParam("realm") @NotBlank String realmName + ) { + log.info("GET /api/roles/client/{}/{} - realm: {}", clientId, roleName, realmName); + + try { + return roleService.getClientRoleByName(roleName, clientId, realmName) + .map(role -> Response.ok(role).build()) + .orElse(Response.status(Response.Status.NOT_FOUND) + .entity(new ErrorResponse("Rôle client non trouvé")) + .build()); + } catch (Exception e) { + log.error("Erreur lors de la récupération du rôle client {}/{}", clientId, roleName, e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(new ErrorResponse(e.getMessage())) + .build(); + } + } + + @GET + @Path("/client/{clientId}") + @Operation(summary = "Lister tous les rôles d'un client", description = "Liste tous les rôles d'un client spécifique") + @APIResponses({ + @APIResponse(responseCode = "200", description = "Liste des rôles"), + @APIResponse(responseCode = "500", description = "Erreur serveur") + }) + @RolesAllowed({"admin", "role_manager", "role_viewer"}) + public Response getAllClientRoles( + @PathParam("clientId") @NotBlank String clientId, + @QueryParam("realm") @NotBlank String realmName + ) { + log.info("GET /api/roles/client/{} - realm: {}", clientId, realmName); + + try { + List roles = roleService.getAllClientRoles(clientId, realmName); + return Response.ok(roles).build(); + } catch (Exception e) { + log.error("Erreur lors de la récupération des rôles du client {}", clientId, e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(new ErrorResponse(e.getMessage())) + .build(); + } + } + + @DELETE + @Path("/client/{clientId}/{roleName}") + @Operation(summary = "Supprimer un rôle client", description = "Supprime un rôle d'un client") + @APIResponses({ + @APIResponse(responseCode = "204", description = "Rôle supprimé"), + @APIResponse(responseCode = "404", description = "Rôle non trouvé"), + @APIResponse(responseCode = "500", description = "Erreur serveur") + }) + @RolesAllowed({"admin"}) + public Response deleteClientRole( + @PathParam("clientId") @NotBlank String clientId, + @PathParam("roleName") @NotBlank String roleName, + @QueryParam("realm") @NotBlank String realmName + ) { + log.info("DELETE /api/roles/client/{}/{} - realm: {}", clientId, roleName, realmName); + + try { + roleService.deleteClientRole(roleName, clientId, realmName); + return Response.noContent().build(); + } catch (Exception e) { + log.error("Erreur lors de la suppression du rôle client {}/{}", clientId, roleName, e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(new ErrorResponse(e.getMessage())) + .build(); + } + } + + // ==================== Endpoints Attribution de rôles ==================== + + @POST + @Path("/assign/realm/{userId}") + @Operation(summary = "Attribuer des rôles realm à un utilisateur", description = "Assigne un ou plusieurs rôles realm à un utilisateur") + @APIResponses({ + @APIResponse(responseCode = "204", description = "Rôles attribués"), + @APIResponse(responseCode = "404", description = "Utilisateur ou rôle non trouvé"), + @APIResponse(responseCode = "500", description = "Erreur serveur") + }) + @RolesAllowed({"admin", "role_manager"}) + public Response assignRealmRoles( + @PathParam("userId") @NotBlank String userId, + @QueryParam("realm") @NotBlank String realmName, + @NotNull RoleAssignmentRequest request + ) { + log.info("POST /api/roles/assign/realm/{} - Attribution de {} rôles", userId, request.roleNames.size()); + + try { + roleService.assignRealmRolesToUser(userId, request.roleNames, realmName); + return Response.noContent().build(); + } catch (Exception e) { + log.error("Erreur lors de l'attribution des rôles realm à l'utilisateur {}", userId, e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(new ErrorResponse(e.getMessage())) + .build(); + } + } + + @POST + @Path("/revoke/realm/{userId}") + @Operation(summary = "Révoquer des rôles realm d'un utilisateur", description = "Révoque un ou plusieurs rôles realm d'un utilisateur") + @APIResponses({ + @APIResponse(responseCode = "204", description = "Rôles révoqués"), + @APIResponse(responseCode = "404", description = "Utilisateur ou rôle non trouvé"), + @APIResponse(responseCode = "500", description = "Erreur serveur") + }) + @RolesAllowed({"admin", "role_manager"}) + public Response revokeRealmRoles( + @PathParam("userId") @NotBlank String userId, + @QueryParam("realm") @NotBlank String realmName, + @NotNull RoleAssignmentRequest request + ) { + log.info("POST /api/roles/revoke/realm/{} - Révocation de {} rôles", userId, request.roleNames.size()); + + try { + roleService.revokeRealmRolesFromUser(userId, request.roleNames, realmName); + return Response.noContent().build(); + } catch (Exception e) { + log.error("Erreur lors de la révocation des rôles realm de l'utilisateur {}", userId, e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(new ErrorResponse(e.getMessage())) + .build(); + } + } + + @POST + @Path("/assign/client/{clientId}/{userId}") + @Operation(summary = "Attribuer des rôles client à un utilisateur", description = "Assigne un ou plusieurs rôles client à un utilisateur") + @APIResponses({ + @APIResponse(responseCode = "204", description = "Rôles attribués"), + @APIResponse(responseCode = "404", description = "Utilisateur, client ou rôle non trouvé"), + @APIResponse(responseCode = "500", description = "Erreur serveur") + }) + @RolesAllowed({"admin", "role_manager"}) + public Response assignClientRoles( + @PathParam("clientId") @NotBlank String clientId, + @PathParam("userId") @NotBlank String userId, + @QueryParam("realm") @NotBlank String realmName, + @NotNull RoleAssignmentRequest request + ) { + log.info("POST /api/roles/assign/client/{}/{} - Attribution de {} rôles client", + clientId, userId, request.roleNames.size()); + + try { + roleService.assignClientRolesToUser(userId, clientId, request.roleNames, realmName); + return Response.noContent().build(); + } catch (Exception e) { + log.error("Erreur lors de l'attribution des rôles client à l'utilisateur {}", userId, e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(new ErrorResponse(e.getMessage())) + .build(); + } + } + + @GET + @Path("/user/realm/{userId}") + @Operation(summary = "Récupérer les rôles realm d'un utilisateur", description = "Liste tous les rôles realm d'un utilisateur") + @APIResponses({ + @APIResponse(responseCode = "200", description = "Liste des rôles"), + @APIResponse(responseCode = "500", description = "Erreur serveur") + }) + @RolesAllowed({"admin", "role_manager", "role_viewer"}) + public Response getUserRealmRoles( + @PathParam("userId") @NotBlank String userId, + @QueryParam("realm") @NotBlank String realmName + ) { + log.info("GET /api/roles/user/realm/{} - realm: {}", userId, realmName); + + try { + List roles = roleService.getUserRealmRoles(userId, realmName); + return Response.ok(roles).build(); + } catch (Exception e) { + log.error("Erreur lors de la récupération des rôles realm de l'utilisateur {}", userId, e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(new ErrorResponse(e.getMessage())) + .build(); + } + } + + @GET + @Path("/user/client/{clientId}/{userId}") + @Operation(summary = "Récupérer les rôles client d'un utilisateur", description = "Liste tous les rôles client d'un utilisateur") + @APIResponses({ + @APIResponse(responseCode = "200", description = "Liste des rôles"), + @APIResponse(responseCode = "500", description = "Erreur serveur") + }) + @RolesAllowed({"admin", "role_manager", "role_viewer"}) + public Response getUserClientRoles( + @PathParam("clientId") @NotBlank String clientId, + @PathParam("userId") @NotBlank String userId, + @QueryParam("realm") @NotBlank String realmName + ) { + log.info("GET /api/roles/user/client/{}/{} - realm: {}", clientId, userId, realmName); + + try { + List roles = roleService.getUserClientRoles(userId, clientId, realmName); + return Response.ok(roles).build(); + } catch (Exception e) { + log.error("Erreur lors de la récupération des rôles client de l'utilisateur {}", userId, e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(new ErrorResponse(e.getMessage())) + .build(); + } + } + + // ==================== Endpoints Rôles composites ==================== + + @POST + @Path("/composite/{roleName}/add") + @Operation(summary = "Ajouter des rôles composites", description = "Ajoute des rôles composites à un rôle") + @APIResponses({ + @APIResponse(responseCode = "204", description = "Composites ajoutés"), + @APIResponse(responseCode = "404", description = "Rôle non trouvé"), + @APIResponse(responseCode = "500", description = "Erreur serveur") + }) + @RolesAllowed({"admin", "role_manager"}) + public Response addComposites( + @PathParam("roleName") @NotBlank String roleName, + @QueryParam("realm") @NotBlank String realmName, + @NotNull RoleAssignmentRequest request + ) { + log.info("POST /api/roles/composite/{}/add - Ajout de {} composites", roleName, request.roleNames.size()); + + try { + roleService.addCompositesToRealmRole(roleName, request.roleNames, realmName); + return Response.noContent().build(); + } catch (Exception e) { + log.error("Erreur lors de l'ajout des composites au rôle {}", roleName, e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(new ErrorResponse(e.getMessage())) + .build(); + } + } + + @GET + @Path("/composite/{roleName}") + @Operation(summary = "Récupérer les rôles composites", description = "Liste tous les rôles composites d'un rôle") + @APIResponses({ + @APIResponse(responseCode = "200", description = "Liste des composites"), + @APIResponse(responseCode = "500", description = "Erreur serveur") + }) + @RolesAllowed({"admin", "role_manager", "role_viewer"}) + public Response getComposites( + @PathParam("roleName") @NotBlank String roleName, + @QueryParam("realm") @NotBlank String realmName + ) { + log.info("GET /api/roles/composite/{} - realm: {}", roleName, realmName); + + try { + List composites = roleService.getCompositeRoles(roleName, realmName); + return Response.ok(composites).build(); + } catch (Exception e) { + log.error("Erreur lors de la récupération des composites du rôle {}", roleName, e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(new ErrorResponse(e.getMessage())) + .build(); + } + } + + // ==================== DTOs internes ==================== + + @Schema(description = "Requête d'attribution/révocation de rôles") + public static class RoleAssignmentRequest { + @Schema(description = "Liste des noms de rôles", required = true) + public List roleNames; + } + + @Schema(description = "Réponse d'erreur") + public static class ErrorResponse { + @Schema(description = "Message d'erreur") + public String message; + + public ErrorResponse(String message) { + this.message = message; + } + } +} diff --git a/src/main/java/dev/lions/user/manager/resource/UserResource.java b/src/main/java/dev/lions/user/manager/resource/UserResource.java new file mode 100644 index 0000000..0eb912b --- /dev/null +++ b/src/main/java/dev/lions/user/manager/resource/UserResource.java @@ -0,0 +1,406 @@ +package dev.lions.user.manager.resource; + +import dev.lions.user.manager.dto.user.UserDTO; +import dev.lions.user.manager.dto.user.UserSearchCriteriaDTO; +import dev.lions.user.manager.dto.user.UserSearchResultDTO; +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.*; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.media.Content; +import org.eclipse.microprofile.openapi.annotations.media.Schema; +import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponses; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; + +import java.util.List; + +/** + * REST Resource pour la gestion des utilisateurs + * Endpoints exposés pour les opérations CRUD sur les utilisateurs Keycloak + */ +@Path("/api/users") +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +@Tag(name = "Users", description = "Gestion des utilisateurs Keycloak") +@Slf4j +public class UserResource { + + @Inject + UserService userService; + + @POST + @Path("/search") + @Operation(summary = "Rechercher des utilisateurs", description = "Recherche d'utilisateurs selon des critères") + @APIResponses({ + @APIResponse(responseCode = "200", description = "Résultats de recherche", + content = @Content(schema = @Schema(implementation = UserSearchResultDTO.class))), + @APIResponse(responseCode = "400", description = "Critères invalides"), + @APIResponse(responseCode = "500", description = "Erreur serveur") + }) + @RolesAllowed({"admin", "user_manager"}) + public Response searchUsers(@Valid @NotNull UserSearchCriteriaDTO criteria) { + log.info("POST /api/users/search - Recherche d'utilisateurs"); + + try { + UserSearchResultDTO result = userService.searchUsers(criteria); + return Response.ok(result).build(); + } catch (Exception e) { + log.error("Erreur lors de la recherche d'utilisateurs", e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(new ErrorResponse(e.getMessage())) + .build(); + } + } + + @GET + @Path("/{userId}") + @Operation(summary = "Récupérer un utilisateur par ID", description = "Récupère les détails d'un utilisateur") + @APIResponses({ + @APIResponse(responseCode = "200", description = "Utilisateur trouvé", + content = @Content(schema = @Schema(implementation = UserDTO.class))), + @APIResponse(responseCode = "404", description = "Utilisateur non trouvé"), + @APIResponse(responseCode = "500", description = "Erreur serveur") + }) + @RolesAllowed({"admin", "user_manager", "user_viewer"}) + public Response getUserById( + @Parameter(description = "ID de l'utilisateur") @PathParam("userId") @NotBlank String userId, + @Parameter(description = "Nom du realm") @QueryParam("realm") @NotBlank String realmName + ) { + log.info("GET /api/users/{} - realm: {}", userId, realmName); + + try { + return userService.getUserById(userId, realmName) + .map(user -> Response.ok(user).build()) + .orElse(Response.status(Response.Status.NOT_FOUND) + .entity(new ErrorResponse("Utilisateur non trouvé")) + .build()); + } catch (Exception e) { + log.error("Erreur lors de la récupération de l'utilisateur {}", userId, e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(new ErrorResponse(e.getMessage())) + .build(); + } + } + + @GET + @Operation(summary = "Lister tous les utilisateurs", description = "Liste paginée de tous les utilisateurs") + @APIResponses({ + @APIResponse(responseCode = "200", description = "Liste des utilisateurs", + content = @Content(schema = @Schema(implementation = UserSearchResultDTO.class))), + @APIResponse(responseCode = "500", description = "Erreur serveur") + }) + @RolesAllowed({"admin", "user_manager", "user_viewer"}) + public Response getAllUsers( + @QueryParam("realm") @NotBlank String realmName, + @QueryParam("page") @DefaultValue("0") int page, + @QueryParam("pageSize") @DefaultValue("20") int pageSize + ) { + log.info("GET /api/users - realm: {}, page: {}, pageSize: {}", realmName, page, pageSize); + + try { + UserSearchResultDTO result = userService.getAllUsers(realmName, page, pageSize); + return Response.ok(result).build(); + } catch (Exception e) { + log.error("Erreur lors de la récupération des utilisateurs", e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(new ErrorResponse(e.getMessage())) + .build(); + } + } + + @POST + @Operation(summary = "Créer un utilisateur", description = "Crée un nouvel utilisateur dans Keycloak") + @APIResponses({ + @APIResponse(responseCode = "201", description = "Utilisateur créé", + content = @Content(schema = @Schema(implementation = UserDTO.class))), + @APIResponse(responseCode = "400", description = "Données invalides"), + @APIResponse(responseCode = "409", description = "Utilisateur existe déjà"), + @APIResponse(responseCode = "500", description = "Erreur serveur") + }) + @RolesAllowed({"admin", "user_manager"}) + public Response createUser( + @Valid @NotNull UserDTO user, + @QueryParam("realm") @NotBlank 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 ErrorResponse(e.getMessage())) + .build(); + } catch (Exception e) { + log.error("Erreur lors de la création de l'utilisateur", e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(new ErrorResponse(e.getMessage())) + .build(); + } + } + + @PUT + @Path("/{userId}") + @Operation(summary = "Mettre à jour un utilisateur", description = "Met à jour les informations d'un utilisateur") + @APIResponses({ + @APIResponse(responseCode = "200", description = "Utilisateur mis à jour", + content = @Content(schema = @Schema(implementation = UserDTO.class))), + @APIResponse(responseCode = "404", description = "Utilisateur non trouvé"), + @APIResponse(responseCode = "400", description = "Données invalides"), + @APIResponse(responseCode = "500", description = "Erreur serveur") + }) + @RolesAllowed({"admin", "user_manager"}) + public Response updateUser( + @PathParam("userId") @NotBlank String userId, + @Valid @NotNull UserDTO user, + @QueryParam("realm") @NotBlank String realmName + ) { + log.info("PUT /api/users/{} - Mise à jour", userId); + + try { + UserDTO updatedUser = userService.updateUser(userId, user, realmName); + return Response.ok(updatedUser).build(); + } catch (RuntimeException e) { + if (e.getMessage().contains("non trouvé")) { + return Response.status(Response.Status.NOT_FOUND) + .entity(new ErrorResponse(e.getMessage())) + .build(); + } + log.error("Erreur lors de la mise à jour de l'utilisateur {}", userId, e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(new ErrorResponse(e.getMessage())) + .build(); + } + } + + @DELETE + @Path("/{userId}") + @Operation(summary = "Supprimer un utilisateur", description = "Supprime un utilisateur (soft ou hard delete)") + @APIResponses({ + @APIResponse(responseCode = "204", description = "Utilisateur supprimé"), + @APIResponse(responseCode = "404", description = "Utilisateur non trouvé"), + @APIResponse(responseCode = "500", description = "Erreur serveur") + }) + @RolesAllowed({"admin"}) + public Response deleteUser( + @PathParam("userId") @NotBlank String userId, + @QueryParam("realm") @NotBlank String realmName, + @QueryParam("hardDelete") @DefaultValue("false") boolean hardDelete + ) { + log.info("DELETE /api/users/{} - realm: {}, hardDelete: {}", userId, realmName, hardDelete); + + try { + userService.deleteUser(userId, realmName, hardDelete); + return Response.noContent().build(); + } catch (RuntimeException e) { + if (e.getMessage().contains("non trouvé")) { + return Response.status(Response.Status.NOT_FOUND) + .entity(new ErrorResponse(e.getMessage())) + .build(); + } + log.error("Erreur lors de la suppression de l'utilisateur {}", userId, e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(new ErrorResponse(e.getMessage())) + .build(); + } + } + + @POST + @Path("/{userId}/activate") + @Operation(summary = "Activer un utilisateur", description = "Active un utilisateur désactivé") + @APIResponses({ + @APIResponse(responseCode = "204", description = "Utilisateur activé"), + @APIResponse(responseCode = "404", description = "Utilisateur non trouvé"), + @APIResponse(responseCode = "500", description = "Erreur serveur") + }) + @RolesAllowed({"admin", "user_manager"}) + public Response activateUser( + @PathParam("userId") @NotBlank String userId, + @QueryParam("realm") @NotBlank String realmName + ) { + log.info("POST /api/users/{}/activate", userId); + + try { + userService.activateUser(userId, realmName); + return Response.noContent().build(); + } catch (Exception e) { + log.error("Erreur lors de l'activation de l'utilisateur {}", userId, e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(new ErrorResponse(e.getMessage())) + .build(); + } + } + + @POST + @Path("/{userId}/deactivate") + @Operation(summary = "Désactiver un utilisateur", description = "Désactive un utilisateur") + @APIResponses({ + @APIResponse(responseCode = "204", description = "Utilisateur désactivé"), + @APIResponse(responseCode = "404", description = "Utilisateur non trouvé"), + @APIResponse(responseCode = "500", description = "Erreur serveur") + }) + @RolesAllowed({"admin", "user_manager"}) + public Response deactivateUser( + @PathParam("userId") @NotBlank String userId, + @QueryParam("realm") @NotBlank String realmName, + @QueryParam("raison") String raison + ) { + log.info("POST /api/users/{}/deactivate - raison: {}", userId, raison); + + try { + userService.deactivateUser(userId, realmName, raison); + return Response.noContent().build(); + } catch (Exception e) { + log.error("Erreur lors de la désactivation de l'utilisateur {}", userId, e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(new ErrorResponse(e.getMessage())) + .build(); + } + } + + @POST + @Path("/{userId}/reset-password") + @Operation(summary = "Réinitialiser le mot de passe", description = "Définit un nouveau mot de passe pour l'utilisateur") + @APIResponses({ + @APIResponse(responseCode = "204", description = "Mot de passe réinitialisé"), + @APIResponse(responseCode = "404", description = "Utilisateur non trouvé"), + @APIResponse(responseCode = "500", description = "Erreur serveur") + }) + @RolesAllowed({"admin", "user_manager"}) + public Response resetPassword( + @PathParam("userId") @NotBlank String userId, + @QueryParam("realm") @NotBlank String realmName, + @NotNull PasswordResetRequest request + ) { + log.info("POST /api/users/{}/reset-password - temporary: {}", userId, request.temporary); + + try { + userService.resetPassword(userId, realmName, request.password, request.temporary); + return Response.noContent().build(); + } catch (Exception e) { + log.error("Erreur lors de la réinitialisation du mot de passe pour {}", userId, e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(new ErrorResponse(e.getMessage())) + .build(); + } + } + + @POST + @Path("/{userId}/send-verification-email") + @Operation(summary = "Envoyer email de vérification", description = "Envoie un email de vérification à l'utilisateur") + @APIResponses({ + @APIResponse(responseCode = "204", description = "Email envoyé"), + @APIResponse(responseCode = "404", description = "Utilisateur non trouvé"), + @APIResponse(responseCode = "500", description = "Erreur serveur") + }) + @RolesAllowed({"admin", "user_manager"}) + public Response sendVerificationEmail( + @PathParam("userId") @NotBlank String userId, + @QueryParam("realm") @NotBlank String realmName + ) { + log.info("POST /api/users/{}/send-verification-email", userId); + + try { + userService.sendVerificationEmail(userId, realmName); + return Response.noContent().build(); + } catch (Exception e) { + log.error("Erreur lors de l'envoi de l'email de vérification pour {}", userId, e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(new ErrorResponse(e.getMessage())) + .build(); + } + } + + @POST + @Path("/{userId}/logout-sessions") + @Operation(summary = "Déconnecter toutes les sessions", description = "Révoque toutes les sessions actives de l'utilisateur") + @APIResponses({ + @APIResponse(responseCode = "200", description = "Sessions révoquées"), + @APIResponse(responseCode = "404", description = "Utilisateur non trouvé"), + @APIResponse(responseCode = "500", description = "Erreur serveur") + }) + @RolesAllowed({"admin", "user_manager"}) + public Response logoutAllSessions( + @PathParam("userId") @NotBlank String userId, + @QueryParam("realm") @NotBlank String realmName + ) { + log.info("POST /api/users/{}/logout-sessions", userId); + + try { + int count = userService.logoutAllSessions(userId, realmName); + return Response.ok(new SessionsRevokedResponse(count)).build(); + } catch (Exception e) { + log.error("Erreur lors de la déconnexion des sessions pour {}", userId, e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(new ErrorResponse(e.getMessage())) + .build(); + } + } + + @GET + @Path("/{userId}/sessions") + @Operation(summary = "Récupérer les sessions actives", description = "Liste les sessions actives de l'utilisateur") + @APIResponses({ + @APIResponse(responseCode = "200", description = "Liste des sessions"), + @APIResponse(responseCode = "404", description = "Utilisateur non trouvé"), + @APIResponse(responseCode = "500", description = "Erreur serveur") + }) + @RolesAllowed({"admin", "user_manager", "user_viewer"}) + public Response getActiveSessions( + @PathParam("userId") @NotBlank String userId, + @QueryParam("realm") @NotBlank String realmName + ) { + log.info("GET /api/users/{}/sessions", userId); + + try { + List sessions = userService.getActiveSessions(userId, realmName); + return Response.ok(sessions).build(); + } catch (Exception e) { + log.error("Erreur lors de la récupération des sessions pour {}", userId, e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(new ErrorResponse(e.getMessage())) + .build(); + } + } + + // ==================== DTOs internes ==================== + + @Schema(description = "Requête de réinitialisation de mot de passe") + public static class PasswordResetRequest { + @Schema(description = "Nouveau mot de passe", required = true) + public String password; + + @Schema(description = "Indique si le mot de passe est temporaire", defaultValue = "true") + public boolean temporary = true; + } + + @Schema(description = "Réponse de révocation de sessions") + public static class SessionsRevokedResponse { + @Schema(description = "Nombre de sessions révoquées") + public int count; + + public SessionsRevokedResponse(int count) { + this.count = count; + } + } + + @Schema(description = "Réponse d'erreur") + public static class ErrorResponse { + @Schema(description = "Message d'erreur") + public String message; + + public ErrorResponse(String message) { + this.message = message; + } + } +} diff --git a/src/main/java/dev/lions/user/manager/service/impl/AuditServiceImpl.java b/src/main/java/dev/lions/user/manager/service/impl/AuditServiceImpl.java new file mode 100644 index 0000000..06ebf1f --- /dev/null +++ b/src/main/java/dev/lions/user/manager/service/impl/AuditServiceImpl.java @@ -0,0 +1,371 @@ +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.service.AuditService; +import jakarta.enterprise.context.ApplicationScoped; +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.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +/** + * Implémentation du service d'audit + * + * NOTES: + * - Cette implémentation utilise un stockage en mémoire pour le développement + * - En production, il faudrait utiliser une base de données (PostgreSQL avec Panache) + * - Les logs sont également écrits via SLF4J pour être capturés par les systèmes de logging centralisés + */ +@ApplicationScoped +@Slf4j +public class AuditServiceImpl implements AuditService { + + // Stockage en mémoire (à remplacer par une DB en production) + private final Map auditLogs = new ConcurrentHashMap<>(); + + @ConfigProperty(name = "lions.audit.enabled", defaultValue = "true") + boolean auditEnabled; + + @ConfigProperty(name = "lions.audit.log-to-database", defaultValue = "false") + boolean logToDatabase; + + @Override + public AuditLogDTO logAction(@Valid @NotNull AuditLogDTO auditLog) { + if (!auditEnabled) { + log.debug("Audit désactivé, log ignoré"); + return auditLog; + } + + // Générer un ID si nécessaire + if (auditLog.getId() == null) { + auditLog.setId(UUID.randomUUID().toString()); + } + + // Ajouter le timestamp si nécessaire + if (auditLog.getDateAction() == null) { + auditLog.setDateAction(LocalDateTime.now()); + } + + // Log structuré pour les systèmes de logging (Graylog, Elasticsearch, etc.) + log.info("AUDIT | Type: {} | Acteur: {} | Ressource: {} | Succès: {} | IP: {} | Détails: {}", + auditLog.getTypeAction(), + auditLog.getActeurUsername(), + auditLog.getRessourceType() + ":" + auditLog.getRessourceId(), + auditLog.isSucces(), + auditLog.getAdresseIp(), + auditLog.getDetails()); + + // Stocker en mémoire + auditLogs.put(auditLog.getId(), auditLog); + + // TODO: Si logToDatabase = true, persister dans PostgreSQL via Panache + // Exemple: + // if (logToDatabase) { + // AuditLogEntity entity = AuditLogMapper.toEntity(auditLog); + // entity.persist(); + // } + + return auditLog; + } + + @Override + public void logSuccess(@NotBlank String acteurUsername, @NotNull TypeActionAudit typeAction, + @NotBlank String ressourceType, @NotBlank String ressourceId, + String adresseIp, String details) { + AuditLogDTO auditLog = AuditLogDTO.builder() + .acteurUsername(acteurUsername) + .typeAction(typeAction) + .ressourceType(ressourceType) + .ressourceId(ressourceId) + .succes(true) + .adresseIp(adresseIp) + .details(details) + .dateAction(LocalDateTime.now()) + .build(); + + logAction(auditLog); + } + + @Override + public void logFailure(@NotBlank String acteurUsername, @NotNull TypeActionAudit typeAction, + @NotBlank String ressourceType, @NotBlank String ressourceId, + String adresseIp, @NotBlank String messageErreur) { + AuditLogDTO auditLog = AuditLogDTO.builder() + .acteurUsername(acteurUsername) + .typeAction(typeAction) + .ressourceType(ressourceType) + .ressourceId(ressourceId) + .succes(false) + .adresseIp(adresseIp) + .messageErreur(messageErreur) + .dateAction(LocalDateTime.now()) + .build(); + + logAction(auditLog); + } + + @Override + public List searchLogs(@NotBlank String acteurUsername, LocalDateTime dateDebut, + LocalDateTime dateFin, TypeActionAudit typeAction, + String ressourceType, Boolean succes, + int page, int pageSize) { + log.debug("Recherche de logs d'audit: acteur={}, dateDebut={}, dateFin={}, typeAction={}, succes={}", + acteurUsername, dateDebut, dateFin, typeAction, succes); + + return auditLogs.values().stream() + .filter(log -> { + // Filtre par acteur (si spécifié et non "*") + if (acteurUsername != null && !"*".equals(acteurUsername) && + !acteurUsername.equals(log.getActeurUsername())) { + return false; + } + + // Filtre par date début + if (dateDebut != null && log.getDateAction().isBefore(dateDebut)) { + return false; + } + + // Filtre par date fin + if (dateFin != null && log.getDateAction().isAfter(dateFin)) { + return false; + } + + // Filtre par type d'action + if (typeAction != null && !typeAction.equals(log.getTypeAction())) { + return false; + } + + // Filtre par type de ressource + if (ressourceType != null && !ressourceType.equals(log.getRessourceType())) { + return false; + } + + // Filtre par succès/échec + if (succes != null && succes != log.isSucces()) { + return false; + } + + return true; + }) + .sorted((a, b) -> b.getDateAction().compareTo(a.getDateAction())) // Tri décroissant par date + .skip((long) page * pageSize) + .limit(pageSize) + .collect(Collectors.toList()); + } + + @Override + public List getLogsByActeur(@NotBlank String acteurUsername, int limit) { + log.debug("Récupération des {} derniers logs de l'acteur: {}", limit, acteurUsername); + + return auditLogs.values().stream() + .filter(log -> acteurUsername.equals(log.getActeurUsername())) + .sorted((a, b) -> b.getDateAction().compareTo(a.getDateAction())) + .limit(limit) + .collect(Collectors.toList()); + } + + @Override + public List getLogsByRessource(@NotBlank String ressourceType, + @NotBlank String ressourceId, int limit) { + log.debug("Récupération des {} derniers logs de la ressource: {}:{}", + limit, ressourceType, ressourceId); + + return auditLogs.values().stream() + .filter(log -> ressourceType.equals(log.getRessourceType()) && + ressourceId.equals(log.getRessourceId())) + .sorted((a, b) -> b.getDateAction().compareTo(a.getDateAction())) + .limit(limit) + .collect(Collectors.toList()); + } + + @Override + public List getLogsByAction(@NotNull TypeActionAudit typeAction, + LocalDateTime dateDebut, LocalDateTime dateFin, + int limit) { + log.debug("Récupération des {} logs de type: {} entre {} et {}", + limit, typeAction, dateDebut, dateFin); + + return auditLogs.values().stream() + .filter(log -> { + if (!typeAction.equals(log.getTypeAction())) { + return false; + } + if (dateDebut != null && log.getDateAction().isBefore(dateDebut)) { + return false; + } + if (dateFin != null && log.getDateAction().isAfter(dateFin)) { + return false; + } + return true; + }) + .sorted((a, b) -> b.getDateAction().compareTo(a.getDateAction())) + .limit(limit) + .collect(Collectors.toList()); + } + + @Override + public Map getActionStatistics(LocalDateTime dateDebut, LocalDateTime dateFin) { + log.debug("Calcul des statistiques d'actions entre {} et {}", dateDebut, dateFin); + + return auditLogs.values().stream() + .filter(log -> { + if (dateDebut != null && log.getDateAction().isBefore(dateDebut)) { + return false; + } + if (dateFin != null && log.getDateAction().isAfter(dateFin)) { + return false; + } + return true; + }) + .collect(Collectors.groupingBy( + AuditLogDTO::getTypeAction, + Collectors.counting() + )); + } + + @Override + public Map getUserActivityStatistics(LocalDateTime dateDebut, LocalDateTime dateFin) { + log.debug("Calcul des statistiques d'activité utilisateurs entre {} et {}", dateDebut, dateFin); + + return auditLogs.values().stream() + .filter(log -> { + if (dateDebut != null && log.getDateAction().isBefore(dateDebut)) { + return false; + } + if (dateFin != null && log.getDateAction().isAfter(dateFin)) { + return false; + } + return true; + }) + .collect(Collectors.groupingBy( + AuditLogDTO::getActeurUsername, + Collectors.counting() + )); + } + + @Override + public long getFailureCount(LocalDateTime dateDebut, LocalDateTime dateFin) { + log.debug("Comptage des échecs entre {} et {}", dateDebut, dateFin); + + return auditLogs.values().stream() + .filter(log -> { + if (log.isSucces()) { + return false; // On ne compte que les échecs + } + if (dateDebut != null && log.getDateAction().isBefore(dateDebut)) { + return false; + } + if (dateFin != null && log.getDateAction().isAfter(dateFin)) { + return false; + } + return true; + }) + .count(); + } + + @Override + public long getSuccessCount(LocalDateTime dateDebut, LocalDateTime dateFin) { + log.debug("Comptage des succès entre {} et {}", dateDebut, dateFin); + + return auditLogs.values().stream() + .filter(log -> { + if (!log.isSucces()) { + return false; // On ne compte que les succès + } + if (dateDebut != null && log.getDateAction().isBefore(dateDebut)) { + return false; + } + if (dateFin != null && log.getDateAction().isAfter(dateFin)) { + return false; + } + return true; + }) + .count(); + } + + @Override + public List exportLogsToCSV(LocalDateTime dateDebut, LocalDateTime dateFin) { + log.info("Export CSV des logs d'audit entre {} et {}", dateDebut, dateFin); + + List csvLines = new ArrayList<>(); + + // En-tête CSV + csvLines.add("ID,Date Action,Acteur,Type Action,Ressource Type,Ressource ID,Succès,Adresse IP,Détails,Message Erreur"); + + // Données + auditLogs.values().stream() + .filter(log -> { + if (dateDebut != null && log.getDateAction().isBefore(dateDebut)) { + return false; + } + if (dateFin != null && log.getDateAction().isAfter(dateFin)) { + return false; + } + return true; + }) + .sorted((a, b) -> a.getDateAction().compareTo(b.getDateAction())) + .forEach(log -> { + String csvLine = String.format("%s,%s,%s,%s,%s,%s,%s,%s,\"%s\",\"%s\"", + log.getId(), + log.getDateAction(), + log.getActeurUsername(), + log.getTypeAction(), + log.getRessourceType(), + log.getRessourceId(), + log.isSucces(), + log.getAdresseIp() != null ? log.getAdresseIp() : "", + log.getDetails() != null ? log.getDetails().replace("\"", "\"\"") : "", + log.getMessageErreur() != null ? log.getMessageErreur().replace("\"", "\"\"") : "" + ); + csvLines.add(csvLine); + }); + + log.info("Export CSV terminé: {} lignes", csvLines.size() - 1); + return csvLines; + } + + @Override + public void purgeOldLogs(int joursDAnc ienneté) { + log.info("Purge des logs d'audit de plus de {} jours", joursDAncienneté); + + LocalDateTime dateLimit = LocalDateTime.now().minusDays(joursDAncienneté); + + long beforeCount = auditLogs.size(); + auditLogs.entrySet().removeIf(entry -> + entry.getValue().getDateAction().isBefore(dateLimit) + ); + long afterCount = auditLogs.size(); + + log.info("Purge terminée: {} logs supprimés", beforeCount - afterCount); + + // TODO: Si base de données utilisée, exécuter: + // DELETE FROM audit_log WHERE date_action < :dateLimit + } + + // ==================== Méthodes utilitaires ==================== + + /** + * Retourne le nombre total de logs en mémoire + */ + public long getTotalCount() { + return auditLogs.size(); + } + + /** + * Vide tous les logs (ATTENTION: à utiliser uniquement en développement) + */ + public void clearAll() { + log.warn("ATTENTION: Suppression de tous les logs d'audit en mémoire"); + auditLogs.clear(); + } +} diff --git a/src/main/java/dev/lions/user/manager/service/impl/RoleServiceImpl.java b/src/main/java/dev/lions/user/manager/service/impl/RoleServiceImpl.java new file mode 100644 index 0000000..d465e4d --- /dev/null +++ b/src/main/java/dev/lions/user/manager/service/impl/RoleServiceImpl.java @@ -0,0 +1,609 @@ +package dev.lions.user.manager.service.impl; + +import dev.lions.user.manager.client.KeycloakAdminClient; +import dev.lions.user.manager.dto.role.RoleAssignmentDTO; +import dev.lions.user.manager.dto.role.RoleDTO; +import dev.lions.user.manager.enums.role.TypeRole; +import dev.lions.user.manager.mapper.RoleMapper; +import dev.lions.user.manager.service.RoleService; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.ws.rs.NotFoundException; +import lombok.extern.slf4j.Slf4j; +import org.keycloak.admin.client.resource.*; +import org.keycloak.representations.idm.RoleRepresentation; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +/** + * Implémentation du service de gestion des rôles Keycloak + */ +@ApplicationScoped +@Slf4j +public class RoleServiceImpl implements RoleService { + + @Inject + KeycloakAdminClient keycloakAdminClient; + + // ==================== CRUD Realm Roles ==================== + + @Override + public RoleDTO createRealmRole(@Valid @NotNull RoleDTO roleDTO, @NotBlank String realmName) { + log.info("Création du rôle realm: {} dans le realm: {}", roleDTO.getNom(), realmName); + + RolesResource rolesResource = keycloakAdminClient.getInstance() + .realm(realmName) + .roles(); + + // Vérifier si le rôle existe déjà + try { + rolesResource.get(roleDTO.getNom()).toRepresentation(); + throw new IllegalArgumentException("Le rôle " + roleDTO.getNom() + " existe déjà"); + } catch (NotFoundException e) { + // OK, le rôle n'existe pas + } + + RoleRepresentation roleRep = RoleMapper.toRepresentation(roleDTO); + rolesResource.create(roleRep); + + // Récupérer le rôle créé avec son ID + RoleRepresentation createdRole = rolesResource.get(roleDTO.getNom()).toRepresentation(); + return RoleMapper.toDTO(createdRole, realmName, TypeRole.REALM_ROLE); + } + + @Override + public Optional getRealmRoleById(@NotBlank String roleId, @NotBlank String realmName) { + log.debug("Récupération du rôle realm par ID: {} dans le realm: {}", roleId, realmName); + + try { + RolesResource rolesResource = keycloakAdminClient.getInstance() + .realm(realmName) + .roles(); + + // Keycloak ne permet pas de récupérer un rôle par ID directement, on doit lister tous les rôles + List roles = rolesResource.list(); + return roles.stream() + .filter(r -> r.getId().equals(roleId)) + .findFirst() + .map(r -> RoleMapper.toDTO(r, realmName, TypeRole.REALM_ROLE)); + } catch (Exception e) { + log.error("Erreur lors de la récupération du rôle realm {}", roleId, e); + return Optional.empty(); + } + } + + @Override + public Optional getRealmRoleByName(@NotBlank String roleName, @NotBlank String realmName) { + log.debug("Récupération du rôle realm par nom: {} dans le realm: {}", roleName, realmName); + + try { + RoleRepresentation roleRep = keycloakAdminClient.getInstance() + .realm(realmName) + .roles() + .get(roleName) + .toRepresentation(); + + return Optional.of(RoleMapper.toDTO(roleRep, realmName, TypeRole.REALM_ROLE)); + } catch (NotFoundException e) { + log.warn("Rôle realm {} non trouvé dans le realm {}", roleName, realmName); + return Optional.empty(); + } + } + + @Override + public RoleDTO updateRealmRole(@NotBlank String roleName, @Valid @NotNull RoleDTO roleDTO, + @NotBlank String realmName) { + log.info("Mise à jour du rôle realm: {} dans le realm: {}", roleName, realmName); + + RoleResource roleResource = keycloakAdminClient.getInstance() + .realm(realmName) + .roles() + .get(roleName); + + RoleRepresentation roleRep = roleResource.toRepresentation(); + + // Mettre à jour uniquement les champs modifiables + if (roleDTO.getDescription() != null) { + roleRep.setDescription(roleDTO.getDescription()); + } + + roleResource.update(roleRep); + + // Retourner le rôle mis à jour + return RoleMapper.toDTO(roleResource.toRepresentation(), realmName, TypeRole.REALM_ROLE); + } + + @Override + public void deleteRealmRole(@NotBlank String roleName, @NotBlank String realmName) { + log.info("Suppression du rôle realm: {} dans le realm: {}", roleName, realmName); + + keycloakAdminClient.getInstance() + .realm(realmName) + .roles() + .deleteRole(roleName); + } + + @Override + public List getAllRealmRoles(@NotBlank String realmName) { + log.debug("Récupération de tous les rôles realm du realm: {}", realmName); + + List roleReps = keycloakAdminClient.getInstance() + .realm(realmName) + .roles() + .list(); + + return RoleMapper.toDTOList(roleReps, realmName, TypeRole.REALM_ROLE); + } + + // ==================== CRUD Client Roles ==================== + + @Override + public RoleDTO createClientRole(@Valid @NotNull RoleDTO roleDTO, @NotBlank String clientId, + @NotBlank String realmName) { + log.info("Création du rôle client: {} pour le client: {} dans le realm: {}", + roleDTO.getNom(), clientId, realmName); + + ClientsResource clientsResource = keycloakAdminClient.getInstance() + .realm(realmName) + .clients(); + + // Trouver le client par clientId + List clients = + clientsResource.findByClientId(clientId); + + if (clients.isEmpty()) { + throw new IllegalArgumentException("Client " + clientId + " non trouvé"); + } + + String internalClientId = clients.get(0).getId(); + RolesResource rolesResource = clientsResource.get(internalClientId).roles(); + + // Vérifier si le rôle existe déjà + try { + rolesResource.get(roleDTO.getNom()).toRepresentation(); + throw new IllegalArgumentException("Le rôle " + roleDTO.getNom() + " existe déjà pour ce client"); + } catch (NotFoundException e) { + // OK, le rôle n'existe pas + } + + RoleRepresentation roleRep = RoleMapper.toRepresentation(roleDTO); + rolesResource.create(roleRep); + + // Récupérer le rôle créé + RoleRepresentation createdRole = rolesResource.get(roleDTO.getNom()).toRepresentation(); + RoleDTO result = RoleMapper.toDTO(createdRole, realmName, TypeRole.CLIENT_ROLE); + result.setClientId(clientId); + + return result; + } + + @Override + public Optional getClientRoleByName(@NotBlank String roleName, @NotBlank String clientId, + @NotBlank String realmName) { + log.debug("Récupération du rôle client: {} pour le client: {} dans le realm: {}", + roleName, clientId, realmName); + + try { + ClientsResource clientsResource = keycloakAdminClient.getInstance() + .realm(realmName) + .clients(); + + List clients = + clientsResource.findByClientId(clientId); + + if (clients.isEmpty()) { + return Optional.empty(); + } + + String internalClientId = clients.get(0).getId(); + RoleRepresentation roleRep = clientsResource.get(internalClientId) + .roles() + .get(roleName) + .toRepresentation(); + + RoleDTO roleDTO = RoleMapper.toDTO(roleRep, realmName, TypeRole.CLIENT_ROLE); + roleDTO.setClientId(clientId); + + return Optional.of(roleDTO); + } catch (NotFoundException e) { + log.warn("Rôle client {} non trouvé pour le client {} dans le realm {}", + roleName, clientId, realmName); + return Optional.empty(); + } + } + + @Override + public void deleteClientRole(@NotBlank String roleName, @NotBlank String clientId, + @NotBlank String realmName) { + log.info("Suppression du rôle client: {} pour le client: {} dans le realm: {}", + roleName, clientId, realmName); + + ClientsResource clientsResource = keycloakAdminClient.getInstance() + .realm(realmName) + .clients(); + + List clients = + clientsResource.findByClientId(clientId); + + if (clients.isEmpty()) { + throw new IllegalArgumentException("Client " + clientId + " non trouvé"); + } + + String internalClientId = clients.get(0).getId(); + clientsResource.get(internalClientId).roles().deleteRole(roleName); + } + + @Override + public List getAllClientRoles(@NotBlank String clientId, @NotBlank String realmName) { + log.debug("Récupération de tous les rôles du client: {} dans le realm: {}", clientId, realmName); + + ClientsResource clientsResource = keycloakAdminClient.getInstance() + .realm(realmName) + .clients(); + + List clients = + clientsResource.findByClientId(clientId); + + if (clients.isEmpty()) { + return List.of(); + } + + String internalClientId = clients.get(0).getId(); + List roleReps = clientsResource.get(internalClientId) + .roles() + .list(); + + List roles = RoleMapper.toDTOList(roleReps, realmName, TypeRole.CLIENT_ROLE); + roles.forEach(role -> role.setClientId(clientId)); + + return roles; + } + + // ==================== Attribution de rôles ==================== + + @Override + public void assignRealmRolesToUser(@NotBlank String userId, @NotNull List roleNames, + @NotBlank String realmName) { + log.info("Attribution de {} rôles realm à l'utilisateur {} dans le realm {}", + roleNames.size(), userId, realmName); + + UserResource userResource = keycloakAdminClient.getInstance() + .realm(realmName) + .users() + .get(userId); + + RolesResource rolesResource = keycloakAdminClient.getInstance() + .realm(realmName) + .roles(); + + List rolesToAssign = roleNames.stream() + .map(roleName -> { + try { + return rolesResource.get(roleName).toRepresentation(); + } catch (NotFoundException e) { + log.warn("Rôle {} non trouvé, ignoré", roleName); + return null; + } + }) + .filter(role -> role != null) + .collect(Collectors.toList()); + + if (!rolesToAssign.isEmpty()) { + userResource.roles().realmLevel().add(rolesToAssign); + } + } + + @Override + public void revokeRealmRolesFromUser(@NotBlank String userId, @NotNull List roleNames, + @NotBlank String realmName) { + log.info("Révocation de {} rôles realm pour l'utilisateur {} dans le realm {}", + roleNames.size(), userId, realmName); + + UserResource userResource = keycloakAdminClient.getInstance() + .realm(realmName) + .users() + .get(userId); + + RolesResource rolesResource = keycloakAdminClient.getInstance() + .realm(realmName) + .roles(); + + List rolesToRevoke = roleNames.stream() + .map(roleName -> { + try { + return rolesResource.get(roleName).toRepresentation(); + } catch (NotFoundException e) { + log.warn("Rôle {} non trouvé, ignoré", roleName); + return null; + } + }) + .filter(role -> role != null) + .collect(Collectors.toList()); + + if (!rolesToRevoke.isEmpty()) { + userResource.roles().realmLevel().remove(rolesToRevoke); + } + } + + @Override + public void assignClientRolesToUser(@NotBlank String userId, @NotBlank String clientId, + @NotNull List roleNames, @NotBlank String realmName) { + log.info("Attribution de {} rôles du client {} à l'utilisateur {} dans le realm {}", + roleNames.size(), clientId, userId, realmName); + + UserResource userResource = keycloakAdminClient.getInstance() + .realm(realmName) + .users() + .get(userId); + + ClientsResource clientsResource = keycloakAdminClient.getInstance() + .realm(realmName) + .clients(); + + List clients = + clientsResource.findByClientId(clientId); + + if (clients.isEmpty()) { + throw new IllegalArgumentException("Client " + clientId + " non trouvé"); + } + + String internalClientId = clients.get(0).getId(); + RolesResource rolesResource = clientsResource.get(internalClientId).roles(); + + List rolesToAssign = roleNames.stream() + .map(roleName -> { + try { + return rolesResource.get(roleName).toRepresentation(); + } catch (NotFoundException e) { + log.warn("Rôle client {} non trouvé, ignoré", roleName); + return null; + } + }) + .filter(role -> role != null) + .collect(Collectors.toList()); + + if (!rolesToAssign.isEmpty()) { + userResource.roles().clientLevel(internalClientId).add(rolesToAssign); + } + } + + @Override + public void revokeClientRolesFromUser(@NotBlank String userId, @NotBlank String clientId, + @NotNull List roleNames, @NotBlank String realmName) { + log.info("Révocation de {} rôles du client {} pour l'utilisateur {} dans le realm {}", + roleNames.size(), clientId, userId, realmName); + + UserResource userResource = keycloakAdminClient.getInstance() + .realm(realmName) + .users() + .get(userId); + + ClientsResource clientsResource = keycloakAdminClient.getInstance() + .realm(realmName) + .clients(); + + List clients = + clientsResource.findByClientId(clientId); + + if (clients.isEmpty()) { + throw new IllegalArgumentException("Client " + clientId + " non trouvé"); + } + + String internalClientId = clients.get(0).getId(); + RolesResource rolesResource = clientsResource.get(internalClientId).roles(); + + List rolesToRevoke = roleNames.stream() + .map(roleName -> { + try { + return rolesResource.get(roleName).toRepresentation(); + } catch (NotFoundException e) { + log.warn("Rôle client {} non trouvé, ignoré", roleName); + return null; + } + }) + .filter(role -> role != null) + .collect(Collectors.toList()); + + if (!rolesToRevoke.isEmpty()) { + userResource.roles().clientLevel(internalClientId).remove(rolesToRevoke); + } + } + + @Override + public List getUserRealmRoles(@NotBlank String userId, @NotBlank String realmName) { + log.debug("Récupération des rôles realm de l'utilisateur {} dans le realm {}", userId, realmName); + + List roleReps = keycloakAdminClient.getInstance() + .realm(realmName) + .users() + .get(userId) + .roles() + .realmLevel() + .listAll(); + + return RoleMapper.toDTOList(roleReps, realmName, TypeRole.REALM_ROLE); + } + + @Override + public List getUserClientRoles(@NotBlank String userId, @NotBlank String clientId, + @NotBlank String realmName) { + log.debug("Récupération des rôles du client {} pour l'utilisateur {} dans le realm {}", + clientId, userId, realmName); + + ClientsResource clientsResource = keycloakAdminClient.getInstance() + .realm(realmName) + .clients(); + + List clients = + clientsResource.findByClientId(clientId); + + if (clients.isEmpty()) { + return List.of(); + } + + String internalClientId = clients.get(0).getId(); + List roleReps = keycloakAdminClient.getInstance() + .realm(realmName) + .users() + .get(userId) + .roles() + .clientLevel(internalClientId) + .listAll(); + + List roles = RoleMapper.toDTOList(roleReps, realmName, TypeRole.CLIENT_ROLE); + roles.forEach(role -> role.setClientId(clientId)); + + return roles; + } + + // ==================== Rôles composites ==================== + + @Override + public void addCompositesToRealmRole(@NotBlank String roleName, @NotNull List compositeRoleNames, + @NotBlank String realmName) { + log.info("Ajout de {} rôles composites au rôle realm {} dans le realm {}", + compositeRoleNames.size(), roleName, realmName); + + RoleResource roleResource = keycloakAdminClient.getInstance() + .realm(realmName) + .roles() + .get(roleName); + + RolesResource rolesResource = keycloakAdminClient.getInstance() + .realm(realmName) + .roles(); + + List compositesToAdd = compositeRoleNames.stream() + .map(compositeName -> { + try { + return rolesResource.get(compositeName).toRepresentation(); + } catch (NotFoundException e) { + log.warn("Rôle composite {} non trouvé, ignoré", compositeName); + return null; + } + }) + .filter(role -> role != null) + .collect(Collectors.toList()); + + if (!compositesToAdd.isEmpty()) { + roleResource.addComposites(compositesToAdd); + } + } + + @Override + public void removeCompositesFromRealmRole(@NotBlank String roleName, @NotNull List compositeRoleNames, + @NotBlank String realmName) { + log.info("Suppression de {} rôles composites du rôle realm {} dans le realm {}", + compositeRoleNames.size(), roleName, realmName); + + RoleResource roleResource = keycloakAdminClient.getInstance() + .realm(realmName) + .roles() + .get(roleName); + + RolesResource rolesResource = keycloakAdminClient.getInstance() + .realm(realmName) + .roles(); + + List compositesToRemove = compositeRoleNames.stream() + .map(compositeName -> { + try { + return rolesResource.get(compositeName).toRepresentation(); + } catch (NotFoundException e) { + log.warn("Rôle composite {} non trouvé, ignoré", compositeName); + return null; + } + }) + .filter(role -> role != null) + .collect(Collectors.toList()); + + if (!compositesToRemove.isEmpty()) { + roleResource.deleteComposites(compositesToRemove); + } + } + + @Override + public List getCompositeRoles(@NotBlank String roleName, @NotBlank String realmName) { + log.debug("Récupération des rôles composites du rôle {} dans le realm {}", roleName, realmName); + + List composites = keycloakAdminClient.getInstance() + .realm(realmName) + .roles() + .get(roleName) + .getRoleComposites(); + + return RoleMapper.toDTOList(composites, realmName, TypeRole.COMPOSITE_ROLE); + } + + // ==================== Vérification de permissions ==================== + + @Override + public boolean userHasRealmRole(@NotBlank String userId, @NotBlank String roleName, + @NotBlank String realmName) { + log.debug("Vérification si l'utilisateur {} a le rôle realm {} dans le realm {}", + userId, roleName, realmName); + + List userRoles = keycloakAdminClient.getInstance() + .realm(realmName) + .users() + .get(userId) + .roles() + .realmLevel() + .listEffective(); // Incluant les rôles hérités via composites + + return userRoles.stream() + .anyMatch(role -> role.getName().equals(roleName)); + } + + @Override + public boolean userHasClientRole(@NotBlank String userId, @NotBlank String clientId, + @NotBlank String roleName, @NotBlank String realmName) { + log.debug("Vérification si l'utilisateur {} a le rôle client {} du client {} dans le realm {}", + userId, roleName, clientId, realmName); + + ClientsResource clientsResource = keycloakAdminClient.getInstance() + .realm(realmName) + .clients(); + + List clients = + clientsResource.findByClientId(clientId); + + if (clients.isEmpty()) { + return false; + } + + String internalClientId = clients.get(0).getId(); + List userClientRoles = keycloakAdminClient.getInstance() + .realm(realmName) + .users() + .get(userId) + .roles() + .clientLevel(internalClientId) + .listEffective(); + + return userClientRoles.stream() + .anyMatch(role -> role.getName().equals(roleName)); + } + + @Override + public List getUserEffectiveRealmRoles(@NotBlank String userId, @NotBlank String realmName) { + log.debug("Récupération des rôles realm effectifs de l'utilisateur {} dans le realm {}", + userId, realmName); + + List effectiveRoles = keycloakAdminClient.getInstance() + .realm(realmName) + .users() + .get(userId) + .roles() + .realmLevel() + .listEffective(); + + return RoleMapper.toDTOList(effectiveRoles, realmName, TypeRole.REALM_ROLE); + } +} diff --git a/src/main/java/dev/lions/user/manager/service/impl/UserServiceImpl.java b/src/main/java/dev/lions/user/manager/service/impl/UserServiceImpl.java new file mode 100644 index 0000000..fba8898 --- /dev/null +++ b/src/main/java/dev/lions/user/manager/service/impl/UserServiceImpl.java @@ -0,0 +1,472 @@ +package dev.lions.user.manager.service.impl; + +import dev.lions.user.manager.client.KeycloakAdminClient; +import dev.lions.user.manager.dto.user.UserDTO; +import dev.lions.user.manager.dto.user.UserSearchCriteriaDTO; +import dev.lions.user.manager.dto.user.UserSearchResultDTO; +import dev.lions.user.manager.mapper.UserMapper; +import dev.lions.user.manager.service.UserService; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.ws.rs.NotFoundException; +import lombok.extern.slf4j.Slf4j; +import org.keycloak.admin.client.resource.UserResource; +import org.keycloak.admin.client.resource.UsersResource; +import org.keycloak.representations.idm.CredentialRepresentation; +import org.keycloak.representations.idm.UserRepresentation; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +/** + * Implémentation du service de gestion des utilisateurs + * Utilise UNIQUEMENT l'API Admin Keycloak (ZERO accès direct à la DB Keycloak) + */ +@ApplicationScoped +@Slf4j +public class UserServiceImpl implements UserService { + + @Inject + KeycloakAdminClient keycloakAdminClient; + + @Override + public UserSearchResultDTO searchUsers(@Valid @NotNull UserSearchCriteriaDTO criteria) { + log.info("Recherche d'utilisateurs avec critères: {}", criteria); + + try { + String realmName = criteria.getRealmName(); + UsersResource usersResource = keycloakAdminClient.getUsers(realmName); + + // Construire la requête de recherche + List users; + + if (criteria.getSearchTerm() != null && !criteria.getSearchTerm().isBlank()) { + // Recherche globale + users = usersResource.search( + criteria.getSearchTerm(), + criteria.getOffset(), + criteria.getPageSize() + ); + } else if (criteria.getUsername() != null) { + // Recherche par username exact + users = usersResource.search( + criteria.getUsername(), + criteria.getOffset(), + criteria.getPageSize(), + true // exact match + ); + } else if (criteria.getEmail() != null) { + // Recherche par email + users = usersResource.searchByEmail( + criteria.getEmail(), + true // exact match + ); + } else { + // Liste tous les utilisateurs + users = usersResource.list( + criteria.getOffset(), + criteria.getPageSize() + ); + } + + // Filtrer selon les critères supplémentaires + users = filterUsers(users, criteria); + + // Convertir en DTOs + List userDTOs = UserMapper.toDTOList(users, realmName); + + // Compter le total + long totalCount = usersResource.count(); + + // Construire le résultat paginé + return UserSearchResultDTO.of(userDTOs, criteria, totalCount); + + } catch (Exception e) { + log.error("Erreur lors de la recherche d'utilisateurs", e); + throw new RuntimeException("Impossible de rechercher les utilisateurs", e); + } + } + + @Override + public Optional getUserById(@NotBlank String userId, @NotBlank String realmName) { + log.info("Récupération de l'utilisateur {} dans le realm {}", userId, realmName); + + try { + UserResource userResource = keycloakAdminClient.getUsers(realmName).get(userId); + UserRepresentation userRep = userResource.toRepresentation(); + return Optional.of(UserMapper.toDTO(userRep, realmName)); + } catch (NotFoundException e) { + log.warn("Utilisateur {} non trouvé dans le realm {}", userId, realmName); + return Optional.empty(); + } catch (Exception e) { + log.error("Erreur lors de la récupération de l'utilisateur {}", userId, e); + throw new RuntimeException("Impossible de récupérer l'utilisateur", e); + } + } + + @Override + public Optional getUserByUsername(@NotBlank String username, @NotBlank String realmName) { + log.info("Récupération de l'utilisateur par username {} dans le realm {}", username, realmName); + + try { + List users = keycloakAdminClient.getUsers(realmName) + .search(username, 0, 1, true); + + if (users.isEmpty()) { + return Optional.empty(); + } + + return Optional.of(UserMapper.toDTO(users.get(0), realmName)); + } catch (Exception e) { + log.error("Erreur lors de la récupération de l'utilisateur par username {}", username, e); + throw new RuntimeException("Impossible de récupérer l'utilisateur", e); + } + } + + @Override + public Optional getUserByEmail(@NotBlank String email, @NotBlank String realmName) { + log.info("Récupération de l'utilisateur par email {} dans le realm {}", email, realmName); + + try { + List users = keycloakAdminClient.getUsers(realmName) + .searchByEmail(email, true); + + if (users.isEmpty()) { + return Optional.empty(); + } + + return Optional.of(UserMapper.toDTO(users.get(0), realmName)); + } catch (Exception e) { + log.error("Erreur lors de la récupération de l'utilisateur par email {}", email, e); + throw new RuntimeException("Impossible de récupérer l'utilisateur", e); + } + } + + @Override + public UserDTO createUser(@Valid @NotNull UserDTO user, @NotBlank String realmName) { + log.info("Création de l'utilisateur {} dans le realm {}", user.getUsername(), realmName); + + try { + // Vérifier si l'utilisateur existe déjà + if (usernameExists(user.getUsername(), realmName)) { + throw new IllegalArgumentException("Le nom d'utilisateur existe déjà: " + user.getUsername()); + } + + if (user.getEmail() != null && emailExists(user.getEmail(), realmName)) { + throw new IllegalArgumentException("L'email existe déjà: " + user.getEmail()); + } + + // Convertir DTO vers UserRepresentation + UserRepresentation userRep = UserMapper.toRepresentation(user); + + // Créer l'utilisateur + UsersResource usersResource = keycloakAdminClient.getUsers(realmName); + var response = usersResource.create(userRep); + + if (response.getStatus() != 201) { + throw new RuntimeException("Échec de la création de l'utilisateur: " + response.getStatusInfo()); + } + + // Récupérer l'ID de l'utilisateur créé + String userId = response.getLocation().getPath().replaceAll(".*/([^/]+)$", "$1"); + + // Définir le mot de passe si fourni + if (user.getTemporaryPassword() != null) { + setPassword(userId, realmName, user.getTemporaryPassword(), + user.getTemporaryPasswordFlag() != null && user.getTemporaryPasswordFlag()); + } + + // Récupérer l'utilisateur créé + UserResource userResource = usersResource.get(userId); + UserRepresentation createdUser = userResource.toRepresentation(); + + log.info("✅ Utilisateur créé avec succès: {} (ID: {})", user.getUsername(), userId); + return UserMapper.toDTO(createdUser, realmName); + + } catch (Exception e) { + log.error("❌ Erreur lors de la création de l'utilisateur {}", user.getUsername(), e); + throw new RuntimeException("Impossible de créer l'utilisateur", e); + } + } + + @Override + public UserDTO updateUser(@NotBlank String userId, @Valid @NotNull UserDTO user, @NotBlank String realmName) { + log.info("Mise à jour de l'utilisateur {} dans le realm {}", userId, realmName); + + try { + UserResource userResource = keycloakAdminClient.getUsers(realmName).get(userId); + + // Récupérer l'utilisateur existant + UserRepresentation existingUser = userResource.toRepresentation(); + + // Mettre à jour les champs + if (user.getEmail() != null) { + existingUser.setEmail(user.getEmail()); + } + if (user.getPrenom() != null) { + existingUser.setFirstName(user.getPrenom()); + } + if (user.getNom() != null) { + existingUser.setLastName(user.getNom()); + } + if (user.getEnabled() != null) { + existingUser.setEnabled(user.getEnabled()); + } + if (user.getEmailVerified() != null) { + existingUser.setEmailVerified(user.getEmailVerified()); + } + if (user.getAttributes() != null) { + existingUser.setAttributes(user.getAttributes()); + } + + // Envoyer la mise à jour + userResource.update(existingUser); + + // Récupérer l'utilisateur mis à jour + UserRepresentation updatedUser = userResource.toRepresentation(); + + log.info("✅ Utilisateur mis à jour avec succès: {}", userId); + return UserMapper.toDTO(updatedUser, realmName); + + } catch (NotFoundException e) { + log.error("❌ Utilisateur {} non trouvé", userId); + throw new RuntimeException("Utilisateur non trouvé", e); + } catch (Exception e) { + log.error("❌ Erreur lors de la mise à jour de l'utilisateur {}", userId, e); + throw new RuntimeException("Impossible de mettre à jour l'utilisateur", e); + } + } + + @Override + public void deleteUser(@NotBlank String userId, @NotBlank String realmName, boolean hardDelete) { + log.info("Suppression de l'utilisateur {} dans le realm {} (hard: {})", userId, realmName, hardDelete); + + try { + UserResource userResource = keycloakAdminClient.getUsers(realmName).get(userId); + + if (hardDelete) { + // Suppression définitive + userResource.remove(); + log.info("✅ Utilisateur supprimé définitivement: {}", userId); + } else { + // Soft delete: désactiver l'utilisateur + UserRepresentation user = userResource.toRepresentation(); + user.setEnabled(false); + userResource.update(user); + log.info("✅ Utilisateur désactivé (soft delete): {}", userId); + } + + } catch (NotFoundException e) { + log.error("❌ Utilisateur {} non trouvé", userId); + throw new RuntimeException("Utilisateur non trouvé", e); + } catch (Exception e) { + log.error("❌ Erreur lors de la suppression de l'utilisateur {}", userId, e); + throw new RuntimeException("Impossible de supprimer l'utilisateur", e); + } + } + + @Override + public void activateUser(@NotBlank String userId, @NotBlank String realmName) { + log.info("Activation de l'utilisateur {} dans le realm {}", userId, realmName); + + try { + UserResource userResource = keycloakAdminClient.getUsers(realmName).get(userId); + UserRepresentation user = userResource.toRepresentation(); + user.setEnabled(true); + userResource.update(user); + + log.info("✅ Utilisateur activé: {}", userId); + } catch (Exception e) { + log.error("❌ Erreur lors de l'activation de l'utilisateur {}", userId, e); + throw new RuntimeException("Impossible d'activer l'utilisateur", e); + } + } + + @Override + public void deactivateUser(@NotBlank String userId, @NotBlank String realmName, String raison) { + log.info("Désactivation de l'utilisateur {} dans le realm {} (raison: {})", userId, realmName, raison); + + try { + UserResource userResource = keycloakAdminClient.getUsers(realmName).get(userId); + UserRepresentation user = userResource.toRepresentation(); + user.setEnabled(false); + userResource.update(user); + + log.info("✅ Utilisateur désactivé: {}", userId); + } catch (Exception e) { + log.error("❌ Erreur lors de la désactivation de l'utilisateur {}", userId, e); + throw new RuntimeException("Impossible de désactiver l'utilisateur", e); + } + } + + @Override + public void suspendUser(@NotBlank String userId, @NotBlank String realmName, String raison, int duree) { + log.info("Suspension de l'utilisateur {} dans le realm {} (raison: {}, durée: {} jours)", + userId, realmName, raison, duree); + + deactivateUser(userId, realmName, raison); + } + + @Override + public void unlockUser(@NotBlank String userId, @NotBlank String realmName) { + log.info("Déverrouillage de l'utilisateur {} dans le realm {}", userId, realmName); + activateUser(userId, realmName); + } + + @Override + public void resetPassword(@NotBlank String userId, @NotBlank String realmName, + @NotBlank String temporaryPassword, boolean temporary) { + log.info("Réinitialisation du mot de passe pour l'utilisateur {} (temporaire: {})", userId, temporary); + + setPassword(userId, realmName, temporaryPassword, temporary); + } + + @Override + public void sendVerificationEmail(@NotBlank String userId, @NotBlank String realmName) { + log.info("Envoi d'un email de vérification à l'utilisateur {}", userId); + + try { + UserResource userResource = keycloakAdminClient.getUsers(realmName).get(userId); + userResource.sendVerifyEmail(); + + log.info("✅ Email de vérification envoyé: {}", userId); + } catch (Exception e) { + log.error("❌ Erreur lors de l'envoi de l'email de vérification pour {}", userId, e); + throw new RuntimeException("Impossible d'envoyer l'email de vérification", e); + } + } + + @Override + public int logoutAllSessions(@NotBlank String userId, @NotBlank String realmName) { + log.info("Déconnexion de toutes les sessions pour l'utilisateur {}", userId); + + try { + UserResource userResource = keycloakAdminClient.getUsers(realmName).get(userId); + int sessionsCount = userResource.getUserSessions().size(); + userResource.logout(); + + log.info("✅ {} sessions révoquées pour l'utilisateur {}", sessionsCount, userId); + return sessionsCount; + } catch (Exception e) { + log.error("❌ Erreur lors de la déconnexion des sessions pour {}", userId, e); + throw new RuntimeException("Impossible de déconnecter les sessions", e); + } + } + + @Override + public List getActiveSessions(@NotBlank String userId, @NotBlank String realmName) { + log.info("Récupération des sessions actives pour l'utilisateur {}", userId); + + try { + UserResource userResource = keycloakAdminClient.getUsers(realmName).get(userId); + return userResource.getUserSessions().stream() + .map(session -> session.getId()) + .collect(Collectors.toList()); + } catch (Exception e) { + log.error("❌ Erreur lors de la récupération des sessions pour {}", userId, e); + return Collections.emptyList(); + } + } + + @Override + public long countUsers(@NotNull UserSearchCriteriaDTO criteria) { + try { + return keycloakAdminClient.getUsers(criteria.getRealmName()).count(); + } catch (Exception e) { + log.error("Erreur lors du comptage des utilisateurs", e); + return 0; + } + } + + @Override + public UserSearchResultDTO getAllUsers(@NotBlank String realmName, int page, int pageSize) { + UserSearchCriteriaDTO criteria = UserSearchCriteriaDTO.builder() + .realmName(realmName) + .page(page) + .pageSize(pageSize) + .build(); + + return searchUsers(criteria); + } + + @Override + public boolean usernameExists(@NotBlank String username, @NotBlank String realmName) { + try { + List users = keycloakAdminClient.getUsers(realmName) + .search(username, 0, 1, true); + return !users.isEmpty(); + } catch (Exception e) { + log.error("Erreur lors de la vérification de l'existence du username {}", username, e); + return false; + } + } + + @Override + public boolean emailExists(@NotBlank String email, @NotBlank String realmName) { + try { + List users = keycloakAdminClient.getUsers(realmName) + .searchByEmail(email, true); + return !users.isEmpty(); + } catch (Exception e) { + log.error("Erreur lors de la vérification de l'existence de l'email {}", email, e); + return false; + } + } + + @Override + public String exportUsersToCSV(@NotNull UserSearchCriteriaDTO criteria) { + // TODO: Implémenter l'export CSV + throw new UnsupportedOperationException("Export CSV non implémenté"); + } + + @Override + public int importUsersFromCSV(@NotBlank String csvContent, @NotBlank String realmName) { + // TODO: Implémenter l'import CSV + throw new UnsupportedOperationException("Import CSV non implémenté"); + } + + // ==================== Méthodes privées ==================== + + private void setPassword(String userId, String realmName, String password, boolean temporary) { + try { + UserResource userResource = keycloakAdminClient.getUsers(realmName).get(userId); + + CredentialRepresentation credential = new CredentialRepresentation(); + credential.setType(CredentialRepresentation.PASSWORD); + credential.setValue(password); + credential.setTemporary(temporary); + + userResource.resetPassword(credential); + + log.info("✅ Mot de passe défini pour l'utilisateur {} (temporaire: {})", userId, temporary); + } catch (Exception e) { + log.error("❌ Erreur lors de la définition du mot de passe pour {}", userId, e); + throw new RuntimeException("Impossible de définir le mot de passe", e); + } + } + + private List filterUsers(List users, UserSearchCriteriaDTO criteria) { + return users.stream() + .filter(user -> { + // Filtrer par enabled + if (criteria.getEnabled() != null && !criteria.getEnabled().equals(user.isEnabled())) { + return false; + } + + // Filtrer par emailVerified + if (criteria.getEmailVerified() != null && !criteria.getEmailVerified().equals(user.isEmailVerified())) { + return false; + } + + // TODO: Ajouter d'autres filtres selon les besoins + + return true; + }) + .collect(Collectors.toList()); + } +} diff --git a/src/main/resources/application-dev.properties b/src/main/resources/application-dev.properties new file mode 100644 index 0000000..836e8d5 --- /dev/null +++ b/src/main/resources/application-dev.properties @@ -0,0 +1,82 @@ +# ============================================================================ +# Lions User Manager - Server Implementation Configuration - DEV +# ============================================================================ + +# HTTP Configuration +quarkus.http.port=8081 +quarkus.http.host=localhost +quarkus.http.cors=true +quarkus.http.cors.origins=http://localhost:3000,http://localhost:8080 +quarkus.http.cors.methods=GET,POST,PUT,DELETE,PATCH,OPTIONS +quarkus.http.cors.headers=* + +# Keycloak OIDC Configuration (DEV) +quarkus.oidc.auth-server-url=http://localhost:8180/realms/master +quarkus.oidc.client-id=lions-user-manager +quarkus.oidc.credentials.secret=dev-secret-change-me +quarkus.oidc.tls.verification=none +quarkus.oidc.application-type=service + +# Keycloak Admin Client Configuration (DEV) +lions.keycloak.server-url=http://localhost:8180 +lions.keycloak.admin-realm=master +lions.keycloak.admin-client-id=admin-cli +lions.keycloak.admin-username=admin +lions.keycloak.admin-password=admin +lions.keycloak.connection-pool-size=5 +lions.keycloak.timeout-seconds=30 + +# Realms autorisés (DEV) +lions.keycloak.authorized-realms=btpxpress,master,lions-realm,test-realm + +# Circuit Breaker Configuration (DEV - plus permissif) +quarkus.smallrye-fault-tolerance.enabled=true + +# Retry Configuration (DEV) +lions.keycloak.retry.max-attempts=3 +lions.keycloak.retry.delay-seconds=1 + +# Audit Configuration (DEV) +lions.audit.enabled=true +lions.audit.log-to-database=false +lions.audit.log-to-file=true +lions.audit.retention-days=30 + +# Database Configuration (DEV - optionnel) +# Décommenter pour utiliser une DB locale +#quarkus.datasource.db-kind=postgresql +#quarkus.datasource.username=postgres +#quarkus.datasource.password=postgres +#quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:5432/lions_audit_dev +#quarkus.hibernate-orm.database.generation=update +#quarkus.flyway.migrate-at-start=false + +# Logging Configuration (DEV) +quarkus.log.level=DEBUG +quarkus.log.category."dev.lions.user.manager".level=DEBUG +quarkus.log.category."org.keycloak".level=INFO +quarkus.log.category."io.quarkus".level=INFO + +quarkus.log.console.enable=true +quarkus.log.console.format=%d{HH:mm:ss} %-5p [%c{2.}] (%t) %s%e%n +quarkus.log.console.color=true + +# File Logging pour Audit (DEV) +quarkus.log.file.enable=true +quarkus.log.file.path=logs/dev/lions-user-manager.log +quarkus.log.file.rotation.max-file-size=10M +quarkus.log.file.rotation.max-backup-index=3 + +# OpenAPI/Swagger Configuration (DEV - toujours activé) +quarkus.swagger-ui.always-include=true +quarkus.swagger-ui.path=/swagger-ui +quarkus.swagger-ui.enable=true + +# Dev Services (activé en DEV) +quarkus.devservices.enabled=false + +# Security Configuration (DEV - plus permissif) +quarkus.security.jaxrs.deny-unannotated-endpoints=false + +# Hot Reload +quarkus.live-reload.instrumentation=true diff --git a/src/main/resources/application-prod.properties b/src/main/resources/application-prod.properties new file mode 100644 index 0000000..df77357 --- /dev/null +++ b/src/main/resources/application-prod.properties @@ -0,0 +1,113 @@ +# ============================================================================ +# Lions User Manager - Server Implementation Configuration - PRODUCTION +# ============================================================================ + +# HTTP Configuration +quarkus.http.port=8081 +quarkus.http.host=0.0.0.0 +quarkus.http.cors=true +quarkus.http.cors.origins=https://btpxpress.lions.dev,https://admin.lions.dev +quarkus.http.cors.methods=GET,POST,PUT,DELETE,PATCH,OPTIONS +quarkus.http.cors.headers=* + +# Keycloak OIDC Configuration (PROD) +quarkus.oidc.auth-server-url=https://security.lions.dev/realms/master +quarkus.oidc.client-id=lions-user-manager +quarkus.oidc.credentials.secret=${KEYCLOAK_CLIENT_SECRET} +quarkus.oidc.tls.verification=required +quarkus.oidc.application-type=service + +# Keycloak Admin Client Configuration (PROD) +lions.keycloak.server-url=https://security.lions.dev +lions.keycloak.admin-realm=master +lions.keycloak.admin-client-id=admin-cli +lions.keycloak.admin-username=${KEYCLOAK_ADMIN_USERNAME} +lions.keycloak.admin-password=${KEYCLOAK_ADMIN_PASSWORD} +lions.keycloak.connection-pool-size=20 +lions.keycloak.timeout-seconds=60 + +# Realms autorisés (PROD) +lions.keycloak.authorized-realms=btpxpress,lions-realm + +# Circuit Breaker Configuration (PROD - strict) +quarkus.smallrye-fault-tolerance.enabled=true + +# Retry Configuration (PROD) +lions.keycloak.retry.max-attempts=5 +lions.keycloak.retry.delay-seconds=3 + +# Audit Configuration (PROD) +lions.audit.enabled=true +lions.audit.log-to-database=true +lions.audit.log-to-file=true +lions.audit.retention-days=365 + +# Database Configuration (PROD - obligatoire pour audit) +quarkus.datasource.db-kind=postgresql +quarkus.datasource.username=${DB_USERNAME:audit_user} +quarkus.datasource.password=${DB_PASSWORD} +quarkus.datasource.jdbc.url=jdbc:postgresql://${DB_HOST:lions-db.lions.svc.cluster.local}:${DB_PORT:5432}/${DB_NAME:lions_audit} +quarkus.datasource.jdbc.max-size=20 +quarkus.datasource.jdbc.min-size=5 +quarkus.hibernate-orm.database.generation=none +quarkus.flyway.migrate-at-start=true +quarkus.flyway.baseline-on-migrate=true +quarkus.flyway.baseline-version=1.0.0 + +# Logging Configuration (PROD) +quarkus.log.level=INFO +quarkus.log.category."dev.lions.user.manager".level=INFO +quarkus.log.category."org.keycloak".level=WARN +quarkus.log.category."io.quarkus".level=WARN + +quarkus.log.console.enable=true +quarkus.log.console.format=%d{yyyy-MM-dd HH:mm:ss,SSS} %-5p [%c{3.}] (%t) %s%e%n +quarkus.log.console.json=true + +# File Logging pour Audit (PROD) +quarkus.log.file.enable=true +quarkus.log.file.path=/var/log/lions/lions-user-manager.log +quarkus.log.file.rotation.max-file-size=50M +quarkus.log.file.rotation.max-backup-index=30 +quarkus.log.file.rotation.rotate-on-boot=false + +# OpenAPI/Swagger Configuration (PROD - désactivé par défaut) +quarkus.swagger-ui.always-include=false +quarkus.swagger-ui.path=/swagger-ui +quarkus.swagger-ui.enable=false + +# Dev Services (désactivé en PROD) +quarkus.devservices.enabled=false + +# Security Configuration (PROD - strict) +quarkus.security.jaxrs.deny-unannotated-endpoints=true + +# Health Check Configuration (PROD) +quarkus.smallrye-health.root-path=/health +quarkus.smallrye-health.liveness-path=/health/live +quarkus.smallrye-health.readiness-path=/health/ready + +# Metrics Configuration (PROD) +quarkus.micrometer.enabled=true +quarkus.micrometer.export.prometheus.enabled=true +quarkus.micrometer.export.prometheus.path=/metrics + +# Jackson Configuration (PROD) +quarkus.jackson.fail-on-unknown-properties=false +quarkus.jackson.write-dates-as-timestamps=false +quarkus.jackson.serialization-inclusion=non_null + +# Performance tuning (PROD) +quarkus.thread-pool.core-threads=2 +quarkus.thread-pool.max-threads=16 +quarkus.thread-pool.queue-size=100 + +# SSL/TLS Configuration (PROD) +quarkus.http.ssl.certificate.key-store-file=${SSL_KEYSTORE_FILE:/etc/ssl/keystore.p12} +quarkus.http.ssl.certificate.key-store-password=${SSL_KEYSTORE_PASSWORD} +quarkus.http.ssl.certificate.key-store-file-type=PKCS12 + +# Monitoring & Observability +quarkus.log.handler.gelf.enabled=false +quarkus.log.handler.gelf.host=${GRAYLOG_HOST:logs.lions.dev} +quarkus.log.handler.gelf.port=${GRAYLOG_PORT:12201} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 0000000..0cfcd12 --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1,100 @@ +# ============================================================================ +# Lions User Manager - Server Implementation Configuration +# ============================================================================ + +# Application Info +quarkus.application.name=lions-user-manager-server +quarkus.application.version=1.0.0 + +# HTTP Configuration +quarkus.http.port=8081 +quarkus.http.host=0.0.0.0 +quarkus.http.cors=true +quarkus.http.cors.origins=* +quarkus.http.cors.methods=GET,POST,PUT,DELETE,PATCH,OPTIONS +quarkus.http.cors.headers=* + +# Keycloak OIDC Configuration +quarkus.oidc.auth-server-url=https://security.lions.dev/realms/master +quarkus.oidc.client-id=lions-user-manager +quarkus.oidc.credentials.secret=${KEYCLOAK_CLIENT_SECRET:your-client-secret} +quarkus.oidc.tls.verification=none +quarkus.oidc.application-type=service + +# Keycloak Admin Client Configuration +lions.keycloak.server-url=https://security.lions.dev +lions.keycloak.admin-realm=master +lions.keycloak.admin-client-id=admin-cli +lions.keycloak.admin-username=${KEYCLOAK_ADMIN_USERNAME:admin} +lions.keycloak.admin-password=${KEYCLOAK_ADMIN_PASSWORD:admin} +lions.keycloak.connection-pool-size=10 +lions.keycloak.timeout-seconds=30 + +# Realms autorisés (séparés par virgule) +lions.keycloak.authorized-realms=btpxpress,master,lions-realm + +# Circuit Breaker Configuration +quarkus.smallrye-fault-tolerance.enabled=true + +# Retry Configuration (pour appels Keycloak) +lions.keycloak.retry.max-attempts=3 +lions.keycloak.retry.delay-seconds=2 + +# Audit Configuration +lions.audit.enabled=true +lions.audit.log-to-database=false +lions.audit.log-to-file=true +lions.audit.retention-days=90 + +# Database Configuration (optionnel - pour logs d'audit) +# Décommenter si vous voulez persister les logs d'audit en DB +#quarkus.datasource.db-kind=postgresql +#quarkus.datasource.username=${DB_USERNAME:audit_user} +#quarkus.datasource.password=${DB_PASSWORD:audit_pass} +#quarkus.datasource.jdbc.url=jdbc:postgresql://${DB_HOST:localhost}:${DB_PORT:5432}/${DB_NAME:lions_audit} +#quarkus.hibernate-orm.database.generation=none +#quarkus.flyway.migrate-at-start=true + +# Logging Configuration +quarkus.log.level=INFO +quarkus.log.category."dev.lions.user.manager".level=DEBUG +quarkus.log.category."org.keycloak".level=WARN + +quarkus.log.console.enable=true +quarkus.log.console.format=%d{yyyy-MM-dd HH:mm:ss,SSS} %-5p [%c{3.}] (%t) %s%e%n + +# File Logging pour Audit +quarkus.log.file.enable=true +quarkus.log.file.path=logs/lions-user-manager.log +quarkus.log.file.rotation.max-file-size=10M +quarkus.log.file.rotation.max-backup-index=10 + +# OpenAPI/Swagger Configuration +quarkus.swagger-ui.always-include=true +quarkus.swagger-ui.path=/swagger-ui +mp.openapi.extensions.smallrye.info.title=Lions User Manager API +mp.openapi.extensions.smallrye.info.version=1.0.0 +mp.openapi.extensions.smallrye.info.description=API de gestion centralisée des utilisateurs Keycloak +mp.openapi.extensions.smallrye.info.contact.name=Lions Dev Team +mp.openapi.extensions.smallrye.info.contact.email=contact@lions.dev + +# Health Check Configuration +quarkus.smallrye-health.root-path=/health +quarkus.smallrye-health.liveness-path=/health/live +quarkus.smallrye-health.readiness-path=/health/ready + +# Metrics Configuration +quarkus.micrometer.enabled=true +quarkus.micrometer.export.prometheus.enabled=true +quarkus.micrometer.export.prometheus.path=/metrics + +# Security Configuration +quarkus.security.jaxrs.deny-unannotated-endpoints=false + +# Jackson Configuration +quarkus.jackson.fail-on-unknown-properties=false +quarkus.jackson.write-dates-as-timestamps=false +quarkus.jackson.serialization-inclusion=non_null + +# Dev Services (désactivé en production) +quarkus.devservices.enabled=false diff --git a/target/classes/application-dev.properties b/target/classes/application-dev.properties new file mode 100644 index 0000000..836e8d5 --- /dev/null +++ b/target/classes/application-dev.properties @@ -0,0 +1,82 @@ +# ============================================================================ +# Lions User Manager - Server Implementation Configuration - DEV +# ============================================================================ + +# HTTP Configuration +quarkus.http.port=8081 +quarkus.http.host=localhost +quarkus.http.cors=true +quarkus.http.cors.origins=http://localhost:3000,http://localhost:8080 +quarkus.http.cors.methods=GET,POST,PUT,DELETE,PATCH,OPTIONS +quarkus.http.cors.headers=* + +# Keycloak OIDC Configuration (DEV) +quarkus.oidc.auth-server-url=http://localhost:8180/realms/master +quarkus.oidc.client-id=lions-user-manager +quarkus.oidc.credentials.secret=dev-secret-change-me +quarkus.oidc.tls.verification=none +quarkus.oidc.application-type=service + +# Keycloak Admin Client Configuration (DEV) +lions.keycloak.server-url=http://localhost:8180 +lions.keycloak.admin-realm=master +lions.keycloak.admin-client-id=admin-cli +lions.keycloak.admin-username=admin +lions.keycloak.admin-password=admin +lions.keycloak.connection-pool-size=5 +lions.keycloak.timeout-seconds=30 + +# Realms autorisés (DEV) +lions.keycloak.authorized-realms=btpxpress,master,lions-realm,test-realm + +# Circuit Breaker Configuration (DEV - plus permissif) +quarkus.smallrye-fault-tolerance.enabled=true + +# Retry Configuration (DEV) +lions.keycloak.retry.max-attempts=3 +lions.keycloak.retry.delay-seconds=1 + +# Audit Configuration (DEV) +lions.audit.enabled=true +lions.audit.log-to-database=false +lions.audit.log-to-file=true +lions.audit.retention-days=30 + +# Database Configuration (DEV - optionnel) +# Décommenter pour utiliser une DB locale +#quarkus.datasource.db-kind=postgresql +#quarkus.datasource.username=postgres +#quarkus.datasource.password=postgres +#quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:5432/lions_audit_dev +#quarkus.hibernate-orm.database.generation=update +#quarkus.flyway.migrate-at-start=false + +# Logging Configuration (DEV) +quarkus.log.level=DEBUG +quarkus.log.category."dev.lions.user.manager".level=DEBUG +quarkus.log.category."org.keycloak".level=INFO +quarkus.log.category."io.quarkus".level=INFO + +quarkus.log.console.enable=true +quarkus.log.console.format=%d{HH:mm:ss} %-5p [%c{2.}] (%t) %s%e%n +quarkus.log.console.color=true + +# File Logging pour Audit (DEV) +quarkus.log.file.enable=true +quarkus.log.file.path=logs/dev/lions-user-manager.log +quarkus.log.file.rotation.max-file-size=10M +quarkus.log.file.rotation.max-backup-index=3 + +# OpenAPI/Swagger Configuration (DEV - toujours activé) +quarkus.swagger-ui.always-include=true +quarkus.swagger-ui.path=/swagger-ui +quarkus.swagger-ui.enable=true + +# Dev Services (activé en DEV) +quarkus.devservices.enabled=false + +# Security Configuration (DEV - plus permissif) +quarkus.security.jaxrs.deny-unannotated-endpoints=false + +# Hot Reload +quarkus.live-reload.instrumentation=true diff --git a/target/classes/application-prod.properties b/target/classes/application-prod.properties new file mode 100644 index 0000000..df77357 --- /dev/null +++ b/target/classes/application-prod.properties @@ -0,0 +1,113 @@ +# ============================================================================ +# Lions User Manager - Server Implementation Configuration - PRODUCTION +# ============================================================================ + +# HTTP Configuration +quarkus.http.port=8081 +quarkus.http.host=0.0.0.0 +quarkus.http.cors=true +quarkus.http.cors.origins=https://btpxpress.lions.dev,https://admin.lions.dev +quarkus.http.cors.methods=GET,POST,PUT,DELETE,PATCH,OPTIONS +quarkus.http.cors.headers=* + +# Keycloak OIDC Configuration (PROD) +quarkus.oidc.auth-server-url=https://security.lions.dev/realms/master +quarkus.oidc.client-id=lions-user-manager +quarkus.oidc.credentials.secret=${KEYCLOAK_CLIENT_SECRET} +quarkus.oidc.tls.verification=required +quarkus.oidc.application-type=service + +# Keycloak Admin Client Configuration (PROD) +lions.keycloak.server-url=https://security.lions.dev +lions.keycloak.admin-realm=master +lions.keycloak.admin-client-id=admin-cli +lions.keycloak.admin-username=${KEYCLOAK_ADMIN_USERNAME} +lions.keycloak.admin-password=${KEYCLOAK_ADMIN_PASSWORD} +lions.keycloak.connection-pool-size=20 +lions.keycloak.timeout-seconds=60 + +# Realms autorisés (PROD) +lions.keycloak.authorized-realms=btpxpress,lions-realm + +# Circuit Breaker Configuration (PROD - strict) +quarkus.smallrye-fault-tolerance.enabled=true + +# Retry Configuration (PROD) +lions.keycloak.retry.max-attempts=5 +lions.keycloak.retry.delay-seconds=3 + +# Audit Configuration (PROD) +lions.audit.enabled=true +lions.audit.log-to-database=true +lions.audit.log-to-file=true +lions.audit.retention-days=365 + +# Database Configuration (PROD - obligatoire pour audit) +quarkus.datasource.db-kind=postgresql +quarkus.datasource.username=${DB_USERNAME:audit_user} +quarkus.datasource.password=${DB_PASSWORD} +quarkus.datasource.jdbc.url=jdbc:postgresql://${DB_HOST:lions-db.lions.svc.cluster.local}:${DB_PORT:5432}/${DB_NAME:lions_audit} +quarkus.datasource.jdbc.max-size=20 +quarkus.datasource.jdbc.min-size=5 +quarkus.hibernate-orm.database.generation=none +quarkus.flyway.migrate-at-start=true +quarkus.flyway.baseline-on-migrate=true +quarkus.flyway.baseline-version=1.0.0 + +# Logging Configuration (PROD) +quarkus.log.level=INFO +quarkus.log.category."dev.lions.user.manager".level=INFO +quarkus.log.category."org.keycloak".level=WARN +quarkus.log.category."io.quarkus".level=WARN + +quarkus.log.console.enable=true +quarkus.log.console.format=%d{yyyy-MM-dd HH:mm:ss,SSS} %-5p [%c{3.}] (%t) %s%e%n +quarkus.log.console.json=true + +# File Logging pour Audit (PROD) +quarkus.log.file.enable=true +quarkus.log.file.path=/var/log/lions/lions-user-manager.log +quarkus.log.file.rotation.max-file-size=50M +quarkus.log.file.rotation.max-backup-index=30 +quarkus.log.file.rotation.rotate-on-boot=false + +# OpenAPI/Swagger Configuration (PROD - désactivé par défaut) +quarkus.swagger-ui.always-include=false +quarkus.swagger-ui.path=/swagger-ui +quarkus.swagger-ui.enable=false + +# Dev Services (désactivé en PROD) +quarkus.devservices.enabled=false + +# Security Configuration (PROD - strict) +quarkus.security.jaxrs.deny-unannotated-endpoints=true + +# Health Check Configuration (PROD) +quarkus.smallrye-health.root-path=/health +quarkus.smallrye-health.liveness-path=/health/live +quarkus.smallrye-health.readiness-path=/health/ready + +# Metrics Configuration (PROD) +quarkus.micrometer.enabled=true +quarkus.micrometer.export.prometheus.enabled=true +quarkus.micrometer.export.prometheus.path=/metrics + +# Jackson Configuration (PROD) +quarkus.jackson.fail-on-unknown-properties=false +quarkus.jackson.write-dates-as-timestamps=false +quarkus.jackson.serialization-inclusion=non_null + +# Performance tuning (PROD) +quarkus.thread-pool.core-threads=2 +quarkus.thread-pool.max-threads=16 +quarkus.thread-pool.queue-size=100 + +# SSL/TLS Configuration (PROD) +quarkus.http.ssl.certificate.key-store-file=${SSL_KEYSTORE_FILE:/etc/ssl/keystore.p12} +quarkus.http.ssl.certificate.key-store-password=${SSL_KEYSTORE_PASSWORD} +quarkus.http.ssl.certificate.key-store-file-type=PKCS12 + +# Monitoring & Observability +quarkus.log.handler.gelf.enabled=false +quarkus.log.handler.gelf.host=${GRAYLOG_HOST:logs.lions.dev} +quarkus.log.handler.gelf.port=${GRAYLOG_PORT:12201} diff --git a/target/classes/application.properties b/target/classes/application.properties new file mode 100644 index 0000000..0cfcd12 --- /dev/null +++ b/target/classes/application.properties @@ -0,0 +1,100 @@ +# ============================================================================ +# Lions User Manager - Server Implementation Configuration +# ============================================================================ + +# Application Info +quarkus.application.name=lions-user-manager-server +quarkus.application.version=1.0.0 + +# HTTP Configuration +quarkus.http.port=8081 +quarkus.http.host=0.0.0.0 +quarkus.http.cors=true +quarkus.http.cors.origins=* +quarkus.http.cors.methods=GET,POST,PUT,DELETE,PATCH,OPTIONS +quarkus.http.cors.headers=* + +# Keycloak OIDC Configuration +quarkus.oidc.auth-server-url=https://security.lions.dev/realms/master +quarkus.oidc.client-id=lions-user-manager +quarkus.oidc.credentials.secret=${KEYCLOAK_CLIENT_SECRET:your-client-secret} +quarkus.oidc.tls.verification=none +quarkus.oidc.application-type=service + +# Keycloak Admin Client Configuration +lions.keycloak.server-url=https://security.lions.dev +lions.keycloak.admin-realm=master +lions.keycloak.admin-client-id=admin-cli +lions.keycloak.admin-username=${KEYCLOAK_ADMIN_USERNAME:admin} +lions.keycloak.admin-password=${KEYCLOAK_ADMIN_PASSWORD:admin} +lions.keycloak.connection-pool-size=10 +lions.keycloak.timeout-seconds=30 + +# Realms autorisés (séparés par virgule) +lions.keycloak.authorized-realms=btpxpress,master,lions-realm + +# Circuit Breaker Configuration +quarkus.smallrye-fault-tolerance.enabled=true + +# Retry Configuration (pour appels Keycloak) +lions.keycloak.retry.max-attempts=3 +lions.keycloak.retry.delay-seconds=2 + +# Audit Configuration +lions.audit.enabled=true +lions.audit.log-to-database=false +lions.audit.log-to-file=true +lions.audit.retention-days=90 + +# Database Configuration (optionnel - pour logs d'audit) +# Décommenter si vous voulez persister les logs d'audit en DB +#quarkus.datasource.db-kind=postgresql +#quarkus.datasource.username=${DB_USERNAME:audit_user} +#quarkus.datasource.password=${DB_PASSWORD:audit_pass} +#quarkus.datasource.jdbc.url=jdbc:postgresql://${DB_HOST:localhost}:${DB_PORT:5432}/${DB_NAME:lions_audit} +#quarkus.hibernate-orm.database.generation=none +#quarkus.flyway.migrate-at-start=true + +# Logging Configuration +quarkus.log.level=INFO +quarkus.log.category."dev.lions.user.manager".level=DEBUG +quarkus.log.category."org.keycloak".level=WARN + +quarkus.log.console.enable=true +quarkus.log.console.format=%d{yyyy-MM-dd HH:mm:ss,SSS} %-5p [%c{3.}] (%t) %s%e%n + +# File Logging pour Audit +quarkus.log.file.enable=true +quarkus.log.file.path=logs/lions-user-manager.log +quarkus.log.file.rotation.max-file-size=10M +quarkus.log.file.rotation.max-backup-index=10 + +# OpenAPI/Swagger Configuration +quarkus.swagger-ui.always-include=true +quarkus.swagger-ui.path=/swagger-ui +mp.openapi.extensions.smallrye.info.title=Lions User Manager API +mp.openapi.extensions.smallrye.info.version=1.0.0 +mp.openapi.extensions.smallrye.info.description=API de gestion centralisée des utilisateurs Keycloak +mp.openapi.extensions.smallrye.info.contact.name=Lions Dev Team +mp.openapi.extensions.smallrye.info.contact.email=contact@lions.dev + +# Health Check Configuration +quarkus.smallrye-health.root-path=/health +quarkus.smallrye-health.liveness-path=/health/live +quarkus.smallrye-health.readiness-path=/health/ready + +# Metrics Configuration +quarkus.micrometer.enabled=true +quarkus.micrometer.export.prometheus.enabled=true +quarkus.micrometer.export.prometheus.path=/metrics + +# Security Configuration +quarkus.security.jaxrs.deny-unannotated-endpoints=false + +# Jackson Configuration +quarkus.jackson.fail-on-unknown-properties=false +quarkus.jackson.write-dates-as-timestamps=false +quarkus.jackson.serialization-inclusion=non_null + +# Dev Services (désactivé en production) +quarkus.devservices.enabled=false diff --git a/target/classes/dev/lions/user/manager/client/KeycloakAdminClient.class b/target/classes/dev/lions/user/manager/client/KeycloakAdminClient.class new file mode 100644 index 0000000000000000000000000000000000000000..3d9910a64e443f0207580a091797c75e7d2f9306 GIT binary patch literal 604 zcmbu7%Sr<=7==%&?eyZMUMfC;Og9GC#f20LVnJxpg}XTY8PaAlkW7odnhPJmhZ6sZ z14=1wy2&Az^M#y~&#(6n0M|H+P!qVC(MlSfTQ8TMoXnI}DQjXh*+SmaTVisxxS3_z z4#Qf6U4iagt&}v%rt;xsPDvrqNvXKAzECzHfs435&Rr@O6+o(zu#O!FG}$&GWio+L zJpRQ_3#V;5_>bbq=S!E64E{+qtC)}c8ovcI_}7?|+|V}6;QzzyXg|!Y<@jhOuoL&6 zIYw88ar>%$!B_P*MZ%}*+=}Oz+4CgOa+LgV1lrS($w(XCKHt#aWtnk9fX)05fF{>j lM+0T85t>0;Xb0_JFX%oFf*zt9^a#g6PfBIdDUUVuz5yRTuL1x7 literal 0 HcmV?d00001 diff --git a/target/classes/dev/lions/user/manager/client/KeycloakAdminClientImpl.class b/target/classes/dev/lions/user/manager/client/KeycloakAdminClientImpl.class new file mode 100644 index 0000000000000000000000000000000000000000..7defcdd3c3961afa4ea70f5d3feff56b0b25fd04 GIT binary patch literal 6372 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 Gg8u+{+C&Ec literal 0 HcmV?d00001 diff --git a/target/classes/dev/lions/user/manager/resource/HealthResourceEndpoint.class b/target/classes/dev/lions/user/manager/resource/HealthResourceEndpoint.class new file mode 100644 index 0000000000000000000000000000000000000000..8cc2df46a6621cb8eb2b3d03a7822acf6d23a2c1 GIT binary patch literal 3525 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 Io)&?>0g291umAu6 literal 0 HcmV?d00001 diff --git a/target/classes/dev/lions/user/manager/resource/RoleResource$ErrorResponse.class b/target/classes/dev/lions/user/manager/resource/RoleResource$ErrorResponse.class new file mode 100644 index 0000000000000000000000000000000000000000..a9907eac00905baabfdc5237483d3e3292abbcf6 GIT binary patch literal 742 zcmbVKO>Yx15Pg$`ZnH_hfC8m_N=UQ?;X5rzsGbYS&)byZuibIUpns zT)6R%00}WpBelQ@*4i0Ado=UjjKBW;^clbt+)mIUJkI$<6~@`1n!sL_+Uluz&%rf5 zV>NUI59i?CbMKuOR7pH1=nz(q^@&!6wo~Fz z#|119RtCoMVN*`nztO@zYIK<{#@d^R=ZBs8$cUW1!T)zx^rWNN2o6L(VeR}N9>u=K zvoG&vqd2Fr|I%9a`-KjHLxLNGhyOx?u=LxI35$mu>K885xGRll%iAGY(haeRpq}_e z`MUwBI2i-_VwdFV04-TSy7LjsI}bi!?Y*EDE{R=^spy-$q3`I@4_ubBE2q&|mtFkQ dd6^Eb3Tolnp9ME?eHIqWZsI1oxHSvh`~{Qh!o&ao literal 0 HcmV?d00001 diff --git a/target/classes/dev/lions/user/manager/resource/RoleResource$RoleAssignmentRequest.class b/target/classes/dev/lions/user/manager/resource/RoleResource$RoleAssignmentRequest.class new file mode 100644 index 0000000000000000000000000000000000000000..c2473d8f58ff9eb96c9aebb61fb0652a9780c3a2 GIT binary patch literal 824 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 NwvKyfW20(Z{{?9k@7w?Y literal 0 HcmV?d00001 diff --git a/target/classes/dev/lions/user/manager/resource/UserResource$SessionsRevokedResponse.class b/target/classes/dev/lions/user/manager/resource/UserResource$SessionsRevokedResponse.class new file mode 100644 index 0000000000000000000000000000000000000000..1598153ad01b7a2b2d798edba695da27f672696f GIT binary patch literal 768 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 literal 0 HcmV?d00001 diff --git a/target/classes/dev/lions/user/manager/service/impl/AuditServiceImpl.class b/target/classes/dev/lions/user/manager/service/impl/AuditServiceImpl.class new file mode 100644 index 0000000000000000000000000000000000000000..221c8a65f7db8e4f9b6b61bb9899a788f03ff899 GIT binary patch literal 12566 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 F_&(iK!x)?q#a!OlNwd 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 literal 0 HcmV?d00001 diff --git a/target/lions-user-manager-server-impl-quarkus-1.0.0.jar b/target/lions-user-manager-server-impl-quarkus-1.0.0.jar new file mode 100644 index 0000000000000000000000000000000000000000..34fc61b7ca8a506e58d486786477b515c2ecab84 GIT binary patch literal 32935 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 Mfq`YZ{(SU*0DYjwNdN!< literal 0 HcmV?d00001 diff --git a/target/maven-archiver/pom.properties b/target/maven-archiver/pom.properties new file mode 100644 index 0000000..280c289 --- /dev/null +++ b/target/maven-archiver/pom.properties @@ -0,0 +1,3 @@ +artifactId=lions-user-manager-server-impl-quarkus +groupId=dev.lions.user.manager +version=1.0.0 diff --git a/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst b/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst new file mode 100644 index 0000000..ee7f2e9 --- /dev/null +++ b/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst @@ -0,0 +1,10 @@ +dev\lions\user\manager\resource\UserResource$ErrorResponse.class +dev\lions\user\manager\service\impl\UserServiceImpl.class +dev\lions\user\manager\client\KeycloakAdminClient.class +dev\lions\user\manager\client\KeycloakAdminClientImpl.class +dev\lions\user\manager\resource\UserResource.class +dev\lions\user\manager\resource\KeycloakHealthCheck.class +dev\lions\user\manager\resource\UserResource$SessionsRevokedResponse.class +dev\lions\user\manager\mapper\UserMapper.class +dev\lions\user\manager\resource\HealthResourceEndpoint.class +dev\lions\user\manager\resource\UserResource$PasswordResetRequest.class diff --git a/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst b/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst new file mode 100644 index 0000000..1517ef1 --- /dev/null +++ b/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst @@ -0,0 +1,7 @@ +C:\Users\dadyo\PersonalProjects\lions-workspace\lions-user-manager\lions-user-manager-server-impl-quarkus\src\main\java\dev\lions\user\manager\resource\KeycloakHealthCheck.java +C:\Users\dadyo\PersonalProjects\lions-workspace\lions-user-manager\lions-user-manager-server-impl-quarkus\src\main\java\dev\lions\user\manager\service\impl\UserServiceImpl.java +C:\Users\dadyo\PersonalProjects\lions-workspace\lions-user-manager\lions-user-manager-server-impl-quarkus\src\main\java\dev\lions\user\manager\resource\HealthResourceEndpoint.java +C:\Users\dadyo\PersonalProjects\lions-workspace\lions-user-manager\lions-user-manager-server-impl-quarkus\src\main\java\dev\lions\user\manager\resource\UserResource.java +C:\Users\dadyo\PersonalProjects\lions-workspace\lions-user-manager\lions-user-manager-server-impl-quarkus\src\main\java\dev\lions\user\manager\client\KeycloakAdminClient.java +C:\Users\dadyo\PersonalProjects\lions-workspace\lions-user-manager\lions-user-manager-server-impl-quarkus\src\main\java\dev\lions\user\manager\mapper\UserMapper.java +C:\Users\dadyo\PersonalProjects\lions-workspace\lions-user-manager\lions-user-manager-server-impl-quarkus\src\main\java\dev\lions\user\manager\client\KeycloakAdminClientImpl.java