feat: Module server-impl-quarkus initial

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 <noreply@anthropic.com>
This commit is contained in:
dahoud
2025-11-09 17:06:37 +00:00
commit c89377d12f
38 changed files with 3780 additions and 0 deletions

206
pom.xml Normal file
View File

@@ -0,0 +1,206 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>dev.lions.user.manager</groupId>
<artifactId>lions-user-manager-parent</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>lions-user-manager-server-impl-quarkus</artifactId>
<packaging>jar</packaging>
<name>Lions User Manager - Server Implementation (Quarkus)</name>
<description>Implémentation serveur: Resources REST, Services, Keycloak Admin Client</description>
<dependencies>
<!-- Module API -->
<dependency>
<groupId>dev.lions.user.manager</groupId>
<artifactId>lions-user-manager-server-api</artifactId>
</dependency>
<!-- Quarkus Extensions -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest-jackson</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-oidc</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-security</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-openapi</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-health</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-micrometer-registry-prometheus</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-hibernate-validator</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest-client-jackson</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-fault-tolerance</artifactId>
</dependency>
<!-- Keycloak Admin Client -->
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-admin-client</artifactId>
<version>23.0.3</version>
<exclusions>
<exclusion>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-client</artifactId>
</exclusion>
<exclusion>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-multipart-provider</artifactId>
</exclusion>
<exclusion>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-jackson2-provider</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- Optional: Database for audit logs -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-hibernate-orm-panache</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jdbc-postgresql</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-flyway</artifactId>
<optional>true</optional>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- MapStruct -->
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
</dependency>
<!-- Testing -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>io.quarkus.platform</groupId>
<artifactId>quarkus-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>build</goal>
<goal>generate-code</goal>
<goal>generate-code-tests</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

View File

@@ -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();
}

View File

@@ -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();
}
}

View File

@@ -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<RoleDTO> toDTOList(List<RoleRepresentation> roleReps, String realmName, TypeRole typeRole) {
if (roleReps == null) {
return List.of();
}
return roleReps.stream()
.map(roleRep -> toDTO(roleRep, realmName, typeRole))
.collect(Collectors.toList());
}
/**
* Convertit une liste de RoleDTO en liste de RoleRepresentation
*/
public static List<RoleRepresentation> toRepresentationList(List<RoleDTO> roleDTOs) {
if (roleDTOs == null) {
return List.of();
}
return roleDTOs.stream()
.map(RoleMapper::toRepresentation)
.collect(Collectors.toList());
}
}

View File

@@ -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<String, List<String>> attributes = new HashMap<>();
if (userDTO.getTelephone() != null) {
attributes.put("phone_number", List.of(userDTO.getTelephone()));
}
if (userDTO.getOrganisation() != null) {
attributes.put("organization", List.of(userDTO.getOrganisation()));
}
if (userDTO.getDepartement() != null) {
attributes.put("department", List.of(userDTO.getDepartement()));
}
if (userDTO.getFonction() != null) {
attributes.put("job_title", List.of(userDTO.getFonction()));
}
if (userDTO.getPays() != null) {
attributes.put("country", List.of(userDTO.getPays()));
}
if (userDTO.getVille() != null) {
attributes.put("city", List.of(userDTO.getVille()));
}
if (userDTO.getLangue() != null) {
attributes.put("locale", List.of(userDTO.getLangue()));
}
if (userDTO.getTimezone() != null) {
attributes.put("timezone", List.of(userDTO.getTimezone()));
}
// Ajouter les attributs existants du DTO
if (userDTO.getAttributes() != null) {
attributes.putAll(userDTO.getAttributes());
}
userRep.setAttributes(attributes);
// Actions requises
if (userDTO.getRequiredActions() != null) {
userRep.setRequiredActions(userDTO.getRequiredActions());
}
return userRep;
}
/**
* Convertit une liste de UserRepresentation vers UserDTO
* @param userReps liste de UserRepresentation
* @param realmName nom du realm
* @return liste de UserDTO
*/
public static List<UserDTO> toDTOList(List<UserRepresentation> userReps, String realmName) {
if (userReps == null) {
return new ArrayList<>();
}
return userReps.stream()
.map(userRep -> toDTO(userRep, realmName))
.collect(Collectors.toList());
}
/**
* Récupère la valeur d'un attribut Keycloak
* @param userRep UserRepresentation
* @param attributeName nom de l'attribut
* @return valeur de l'attribut ou null
*/
private static String getAttributeValue(UserRepresentation userRep, String attributeName) {
if (userRep.getAttributes() == null) {
return null;
}
List<String> values = userRep.getAttributes().get(attributeName);
if (values == null || values.isEmpty()) {
return null;
}
return values.get(0);
}
/**
* Convertit un timestamp (millisecondes) vers LocalDateTime
* @param timestamp timestamp en millisecondes
* @return LocalDateTime ou null
*/
private static LocalDateTime convertTimestamp(Long timestamp) {
if (timestamp == null) {
return null;
}
return LocalDateTime.ofInstant(
Instant.ofEpochMilli(timestamp),
ZoneId.systemDefault()
);
}
}

View File

@@ -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<String, Object> getKeycloakHealth() {
Map<String, Object> 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<String, Object> getServiceStatus() {
Map<String, Object> status = new HashMap<>();
status.put("service", "lions-user-manager-server");
status.put("version", "1.0.0");
status.put("status", "UP");
status.put("timestamp", System.currentTimeMillis());
// Health Keycloak
try {
boolean keycloakConnected = keycloakAdminClient.isConnected();
status.put("keycloak", keycloakConnected ? "CONNECTED" : "DISCONNECTED");
} catch (Exception e) {
status.put("keycloak", "ERROR");
status.put("keycloakError", e.getMessage());
}
return status;
}
}

View File

@@ -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();
}
}
}

View File

@@ -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<RoleDTO> 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<RoleDTO> 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<RoleDTO> 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<RoleDTO> 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<RoleDTO> 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<String> 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;
}
}
}

View File

@@ -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<String> 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;
}
}
}

View File

@@ -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<String, AuditLogDTO> 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<AuditLogDTO> 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<AuditLogDTO> 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<AuditLogDTO> 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<AuditLogDTO> 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<TypeActionAudit, Long> 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<String, Long> 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<String> exportLogsToCSV(LocalDateTime dateDebut, LocalDateTime dateFin) {
log.info("Export CSV des logs d'audit entre {} et {}", dateDebut, dateFin);
List<String> 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();
}
}

View File

@@ -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<RoleDTO> 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<RoleRepresentation> 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<RoleDTO> 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<RoleDTO> getAllRealmRoles(@NotBlank String realmName) {
log.debug("Récupération de tous les rôles realm du realm: {}", realmName);
List<RoleRepresentation> 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<org.keycloak.representations.idm.ClientRepresentation> 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<RoleDTO> 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<org.keycloak.representations.idm.ClientRepresentation> 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<org.keycloak.representations.idm.ClientRepresentation> 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<RoleDTO> 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<org.keycloak.representations.idm.ClientRepresentation> clients =
clientsResource.findByClientId(clientId);
if (clients.isEmpty()) {
return List.of();
}
String internalClientId = clients.get(0).getId();
List<RoleRepresentation> roleReps = clientsResource.get(internalClientId)
.roles()
.list();
List<RoleDTO> 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<String> 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<RoleRepresentation> 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<String> 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<RoleRepresentation> 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<String> 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<org.keycloak.representations.idm.ClientRepresentation> 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<RoleRepresentation> 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<String> 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<org.keycloak.representations.idm.ClientRepresentation> 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<RoleRepresentation> 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<RoleDTO> 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<RoleRepresentation> roleReps = keycloakAdminClient.getInstance()
.realm(realmName)
.users()
.get(userId)
.roles()
.realmLevel()
.listAll();
return RoleMapper.toDTOList(roleReps, realmName, TypeRole.REALM_ROLE);
}
@Override
public List<RoleDTO> 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<org.keycloak.representations.idm.ClientRepresentation> clients =
clientsResource.findByClientId(clientId);
if (clients.isEmpty()) {
return List.of();
}
String internalClientId = clients.get(0).getId();
List<RoleRepresentation> roleReps = keycloakAdminClient.getInstance()
.realm(realmName)
.users()
.get(userId)
.roles()
.clientLevel(internalClientId)
.listAll();
List<RoleDTO> 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<String> 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<RoleRepresentation> 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<String> 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<RoleRepresentation> 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<RoleDTO> 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<RoleRepresentation> 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<RoleRepresentation> 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<org.keycloak.representations.idm.ClientRepresentation> clients =
clientsResource.findByClientId(clientId);
if (clients.isEmpty()) {
return false;
}
String internalClientId = clients.get(0).getId();
List<RoleRepresentation> userClientRoles = keycloakAdminClient.getInstance()
.realm(realmName)
.users()
.get(userId)
.roles()
.clientLevel(internalClientId)
.listEffective();
return userClientRoles.stream()
.anyMatch(role -> role.getName().equals(roleName));
}
@Override
public List<RoleDTO> 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<RoleRepresentation> effectiveRoles = keycloakAdminClient.getInstance()
.realm(realmName)
.users()
.get(userId)
.roles()
.realmLevel()
.listEffective();
return RoleMapper.toDTOList(effectiveRoles, realmName, TypeRole.REALM_ROLE);
}
}

View File

@@ -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<UserRepresentation> 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<UserDTO> 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<UserDTO> 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<UserDTO> getUserByUsername(@NotBlank String username, @NotBlank String realmName) {
log.info("Récupération de l'utilisateur par username {} dans le realm {}", username, realmName);
try {
List<UserRepresentation> 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<UserDTO> getUserByEmail(@NotBlank String email, @NotBlank String realmName) {
log.info("Récupération de l'utilisateur par email {} dans le realm {}", email, realmName);
try {
List<UserRepresentation> 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<String> 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<UserRepresentation> 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<UserRepresentation> 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<UserRepresentation> filterUsers(List<UserRepresentation> 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());
}
}

View File

@@ -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

View File

@@ -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}

View File

@@ -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

View File

@@ -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

View File

@@ -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}

View File

@@ -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

View File

@@ -0,0 +1,3 @@
artifactId=lions-user-manager-server-impl-quarkus
groupId=dev.lions.user.manager
version=1.0.0

View File

@@ -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

View File

@@ -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