Migration complète vers PrimeFaces Freya - Corrections des incompatibilités et intégration de primefaces-freya-extension

This commit is contained in:
lionsdev
2025-12-27 00:18:31 +00:00
parent 03984b50c9
commit 2bc1b0f6a5
49 changed files with 9440 additions and 260 deletions

View File

@@ -51,6 +51,12 @@ public interface KeycloakAdminClient {
*/
boolean realmExists(String realmName);
/**
* Récupère tous les realms disponibles dans Keycloak
* @return liste des noms de realms
*/
java.util.List<String> getAllRealms();
/**
* Ferme la connexion Keycloak
*/

View File

@@ -1,11 +1,13 @@
package dev.lions.user.manager.client;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.quarkus.runtime.Startup;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.ws.rs.client.Client;
import jakarta.ws.rs.client.ClientBuilder;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MediaType;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.eclipse.microprofile.faulttolerance.CircuitBreaker;
@@ -19,6 +21,10 @@ import org.keycloak.admin.client.resource.UsersResource;
import jakarta.ws.rs.NotFoundException;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
/**
* Implémentation du client Keycloak Admin
@@ -29,19 +35,19 @@ import java.time.temporal.ChronoUnit;
@Slf4j
public class KeycloakAdminClientImpl implements KeycloakAdminClient {
@ConfigProperty(name = "lions.keycloak.server-url")
@ConfigProperty(name = "lions.keycloak.server-url", defaultValue = "")
String serverUrl;
@ConfigProperty(name = "lions.keycloak.admin-realm")
@ConfigProperty(name = "lions.keycloak.admin-realm", defaultValue = "master")
String adminRealm;
@ConfigProperty(name = "lions.keycloak.admin-client-id")
@ConfigProperty(name = "lions.keycloak.admin-client-id", defaultValue = "admin-cli")
String adminClientId;
@ConfigProperty(name = "lions.keycloak.admin-username")
@ConfigProperty(name = "lions.keycloak.admin-username", defaultValue = "admin")
String adminUsername;
@ConfigProperty(name = "lions.keycloak.admin-password")
@ConfigProperty(name = "lions.keycloak.admin-password", defaultValue = "")
String adminPassword;
@ConfigProperty(name = "lions.keycloak.connection-pool-size", defaultValue = "10")
@@ -54,6 +60,13 @@ public class KeycloakAdminClientImpl implements KeycloakAdminClient {
@PostConstruct
void init() {
// Ne pas initialiser si les propriétés essentielles sont vides (ex: en mode test)
if (serverUrl == null || serverUrl.isEmpty()) {
log.debug("Configuration Keycloak non disponible - mode test ou configuration manquante");
this.keycloak = null;
return;
}
log.info("========================================");
log.info("Initialisation du client Keycloak Admin");
log.info("========================================");
@@ -144,13 +157,70 @@ public class KeycloakAdminClientImpl implements KeycloakAdminClient {
@Override
public boolean realmExists(String realmName) {
try {
getRealm(realmName).toRepresentation();
// Essayer d'obtenir simplement la liste des rôles du realm
// Si le realm n'existe pas, cela lancera une NotFoundException
// Si le realm existe mais a des problèmes de désérialisation, on suppose qu'il existe
getRealm(realmName).roles().list();
return true;
} catch (NotFoundException e) {
log.debug("Realm {} n'existe pas", realmName);
return false;
} catch (Exception e) {
log.error("Erreur lors de la vérification de l'existence du realm {}: {}", realmName, e.getMessage());
return false;
// En cas d'erreur (comme bruteForceStrategy lors de .toRepresentation()),
// on suppose que le realm existe car l'erreur indique qu'on a pu le contacter
log.debug("Erreur lors de la vérification du realm {} (probablement il existe): {}",
realmName, e.getMessage());
return true;
}
}
@Override
@Retry(maxRetries = 3, delay = 2, delayUnit = ChronoUnit.SECONDS)
@Timeout(value = 30, unit = ChronoUnit.SECONDS)
@CircuitBreaker(requestVolumeThreshold = 10, failureRatio = 0.5, delay = 5000)
public List<String> getAllRealms() {
try {
log.debug("Récupération de tous les realms depuis Keycloak via API REST directe");
// Obtenir un token d'accès pour l'API REST
Keycloak keycloakInstance = getInstance();
String accessToken = keycloakInstance.tokenManager().getAccessTokenString();
// Utiliser un client HTTP REST pour appeler directement l'API Keycloak
// et parser uniquement les noms des realms depuis le JSON
Client client = ClientBuilder.newClient();
try {
String realmsUrl = serverUrl + "/admin/realms";
@SuppressWarnings("unchecked")
List<Map<String, Object>> realmsJson = client.target(realmsUrl)
.request(MediaType.APPLICATION_JSON)
.header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken)
.get(List.class);
List<String> realmNames = new ArrayList<>();
if (realmsJson != null) {
for (Map<String, Object> realm : realmsJson) {
Object realmNameObj = realm.get("realm");
if (realmNameObj != null) {
String realmName = realmNameObj.toString();
if (!realmName.isEmpty()) {
realmNames.add(realmName);
}
}
}
realmNames.sort(String::compareTo);
}
log.info("Récupération réussie: {} realms trouvés", realmNames.size());
return realmNames;
} finally {
client.close();
}
} catch (Exception e) {
log.error("Erreur lors de la récupération des realms: {}", e.getMessage(), e);
// En cas d'erreur, retourner une liste vide plutôt que des données fictives
return Collections.emptyList();
}
}

View File

@@ -0,0 +1,279 @@
package dev.lions.user.manager.config;
import io.quarkus.runtime.StartupEvent;
import jakarta.enterprise.event.Observes;
import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.keycloak.admin.client.Keycloak;
import org.keycloak.admin.client.KeycloakBuilder;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.representations.idm.ProtocolMapperRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.RoleRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import java.util.*;
/**
* Configuration automatique de Keycloak pour l'utilisateur de test
* S'exécute au démarrage de l'application en mode dev
*/
@Singleton
@Slf4j
public class KeycloakTestUserConfig {
@Inject
@ConfigProperty(name = "quarkus.profile", defaultValue = "prod")
String profile;
@Inject
@ConfigProperty(name = "lions.keycloak.server-url")
String keycloakServerUrl;
@Inject
@ConfigProperty(name = "lions.keycloak.admin-realm", defaultValue = "master")
String adminRealm;
@Inject
@ConfigProperty(name = "lions.keycloak.admin-username", defaultValue = "admin")
String adminUsername;
@Inject
@ConfigProperty(name = "lions.keycloak.admin-password", defaultValue = "admin")
String adminPassword;
@Inject
@ConfigProperty(name = "lions.keycloak.authorized-realms")
String authorizedRealms;
private static final String TEST_REALM = "lions-user-manager";
private static final String TEST_USER = "test-user";
private static final String TEST_PASSWORD = "test123";
private static final String TEST_EMAIL = "test@lions.dev";
private static final String CLIENT_ID = "lions-user-manager-client";
private static final List<String> REQUIRED_ROLES = Arrays.asList(
"admin", "user_manager", "user_viewer",
"role_manager", "role_viewer", "auditor", "sync_manager"
);
void onStart(@Observes StartupEvent ev) {
// DÉSACTIVÉ: Configuration manuelle via script create-roles-and-assign.sh
// Cette configuration automatique cause des erreurs de compatibilité Keycloak
// (bruteForceStrategy, cpuInfo non reconnus par la version Keycloak client)
log.info("Configuration automatique de Keycloak DÉSACTIVÉE");
log.info("Utiliser le script create-roles-and-assign.sh pour configurer Keycloak manuellement");
return;
/* ANCIEN CODE DÉSACTIVÉ
// Ne s'exécuter qu'en mode dev
if (!"dev".equals(profile) && !"development".equals(profile)) {
log.debug("Mode non-dev détecté ({}), configuration Keycloak ignorée", profile);
return;
}
log.info("Configuration automatique de Keycloak pour l'utilisateur de test...");
Keycloak adminClient = null;
try {
// Connexion en tant qu'admin
adminClient = KeycloakBuilder.builder()
.serverUrl(keycloakServerUrl)
.realm(adminRealm)
.username(adminUsername)
.password(adminPassword)
.clientId("admin-cli")
.build();
// 1. Vérifier/Créer le realm
ensureRealmExists(adminClient);
// 2. Créer les rôles
ensureRolesExist(adminClient);
// 3. Créer l'utilisateur de test
String userId = ensureTestUserExists(adminClient);
// 4. Assigner les rôles
assignRolesToUser(adminClient, userId);
// 5. Vérifier/Créer le client et le mapper
ensureClientAndMapper(adminClient);
log.info("✓ Configuration Keycloak terminée avec succès");
log.info(" Utilisateur de test: {} / {}", TEST_USER, TEST_PASSWORD);
log.info(" Rôles assignés: {}", String.join(", ", REQUIRED_ROLES));
} catch (Exception e) {
log.error("Erreur lors de la configuration Keycloak: {}", e.getMessage(), e);
} finally {
if (adminClient != null) {
adminClient.close();
}
}
*/
}
private void ensureRealmExists(Keycloak adminClient) {
try {
adminClient.realms().realm(TEST_REALM).toRepresentation();
log.debug("Realm '{}' existe déjà", TEST_REALM);
} catch (jakarta.ws.rs.NotFoundException e) {
log.info("Création du realm '{}'...", TEST_REALM);
RealmRepresentation realm = new RealmRepresentation();
realm.setRealm(TEST_REALM);
realm.setEnabled(true);
adminClient.realms().create(realm);
log.info("✓ Realm '{}' créé", TEST_REALM);
}
}
private void ensureRolesExist(Keycloak adminClient) {
var rolesResource = adminClient.realms().realm(TEST_REALM).roles();
for (String roleName : REQUIRED_ROLES) {
try {
rolesResource.get(roleName).toRepresentation();
log.debug("Rôle '{}' existe déjà", roleName);
} catch (jakarta.ws.rs.NotFoundException e) {
log.info("Création du rôle '{}'...", roleName);
RoleRepresentation role = new RoleRepresentation();
role.setName(roleName);
role.setDescription("Rôle " + roleName + " pour lions-user-manager");
rolesResource.create(role);
log.info("✓ Rôle '{}' créé", roleName);
}
}
}
private String ensureTestUserExists(Keycloak adminClient) {
var usersResource = adminClient.realms().realm(TEST_REALM).users();
// Chercher l'utilisateur
List<UserRepresentation> users = usersResource.search(TEST_USER, true);
String userId;
if (users != null && !users.isEmpty()) {
userId = users.get(0).getId();
log.debug("Utilisateur '{}' existe déjà (ID: {})", TEST_USER, userId);
} else {
log.info("Création de l'utilisateur '{}'...", TEST_USER);
UserRepresentation user = new UserRepresentation();
user.setUsername(TEST_USER);
user.setEmail(TEST_EMAIL);
user.setFirstName("Test");
user.setLastName("User");
user.setEnabled(true);
user.setEmailVerified(true);
jakarta.ws.rs.core.Response response = usersResource.create(user);
userId = getCreatedId(response);
// Définir le mot de passe
CredentialRepresentation credential = new CredentialRepresentation();
credential.setType(CredentialRepresentation.PASSWORD);
credential.setValue(TEST_PASSWORD);
credential.setTemporary(false);
usersResource.get(userId).resetPassword(credential);
log.info("✓ Utilisateur '{}' créé (ID: {})", TEST_USER, userId);
}
return userId;
}
private void assignRolesToUser(Keycloak adminClient, String userId) {
var usersResource = adminClient.realms().realm(TEST_REALM).users();
var rolesResource = adminClient.realms().realm(TEST_REALM).roles();
List<RoleRepresentation> rolesToAssign = new ArrayList<>();
for (String roleName : REQUIRED_ROLES) {
RoleRepresentation role = rolesResource.get(roleName).toRepresentation();
rolesToAssign.add(role);
}
usersResource.get(userId).roles().realmLevel().add(rolesToAssign);
log.info("✓ {} rôles assignés à l'utilisateur", rolesToAssign.size());
}
private void ensureClientAndMapper(Keycloak adminClient) {
try {
var clientsResource = adminClient.realms().realm(TEST_REALM).clients();
var clients = clientsResource.findByClientId(CLIENT_ID);
String clientId;
if (clients == null || clients.isEmpty()) {
log.info("Création du client '{}'...", CLIENT_ID);
org.keycloak.representations.idm.ClientRepresentation client = new org.keycloak.representations.idm.ClientRepresentation();
client.setClientId(CLIENT_ID);
client.setName(CLIENT_ID);
client.setDescription("Client OIDC pour lions-user-manager");
client.setEnabled(true);
client.setPublicClient(false);
client.setStandardFlowEnabled(true);
client.setDirectAccessGrantsEnabled(true);
client.setFullScopeAllowed(true); // IMPORTANT: Permet d'inclure tous les rôles dans le token
client.setRedirectUris(java.util.Arrays.asList(
"http://localhost:8080/*",
"http://localhost:8080/auth/callback"
));
client.setWebOrigins(java.util.Arrays.asList("http://localhost:8080"));
client.setSecret("NTuaQpk5E6qiMqAWTFrCOcIkOABzZzKO");
jakarta.ws.rs.core.Response response = clientsResource.create(client);
clientId = getCreatedId(response);
log.info("✓ Client '{}' créé (ID: {})", CLIENT_ID, clientId);
} else {
clientId = clients.get(0).getId();
log.debug("Client '{}' existe déjà (ID: {})", CLIENT_ID, clientId);
}
// Ajouter le scope "roles" par défaut au client
try {
var clientScopesResource = adminClient.realms().realm(TEST_REALM).clientScopes();
var defaultClientScopes = clientScopesResource.findAll();
var rolesScope = defaultClientScopes.stream()
.filter(s -> "roles".equals(s.getName()))
.findFirst();
if (rolesScope.isPresent()) {
var clientResource = clientsResource.get(clientId);
var defaultScopes = clientResource.getDefaultClientScopes();
boolean hasRolesScope = defaultScopes.stream()
.anyMatch(s -> "roles".equals(s.getName()));
if (!hasRolesScope) {
log.info("Ajout du scope 'roles' au client...");
clientResource.addDefaultClientScope(rolesScope.get().getId());
log.info("✓ Scope 'roles' ajouté au client");
} else {
log.debug("Scope 'roles' déjà présent sur le client");
}
} else {
log.warn("Scope 'roles' non trouvé dans les scopes par défaut du realm");
}
} catch (Exception e) {
log.warn("Erreur lors de l'ajout du scope 'roles': {}", e.getMessage());
}
// Le scope "roles" de Keycloak crée automatiquement realm_access.roles
// Pas besoin de mapper personnalisé si on utilise realm_access.roles
// Le mapper personnalisé peut créer des conflits (comme dans unionflow)
log.debug("Le scope 'roles' est utilisé pour créer realm_access.roles automatiquement");
} catch (Exception e) {
log.warn("Erreur lors de la vérification/création du client: {}", e.getMessage(), e);
}
}
private String getCreatedId(jakarta.ws.rs.core.Response response) {
jakarta.ws.rs.core.Response.StatusType statusInfo = response.getStatusInfo();
if (statusInfo.equals(jakarta.ws.rs.core.Response.Status.CREATED)) {
String location = response.getLocation().getPath();
return location.substring(location.lastIndexOf('/') + 1);
}
throw new RuntimeException("Erreur lors de la création: " + statusInfo.getStatusCode());
}
}

View File

@@ -0,0 +1,406 @@
package dev.lions.user.manager.resource;
import dev.lions.user.manager.dto.realm.RealmAssignmentDTO;
import dev.lions.user.manager.service.RealmAuthorizationService;
import jakarta.annotation.security.RolesAllowed;
import jakarta.inject.Inject;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.SecurityContext;
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 affectations de realms aux utilisateurs
* Permet le contrôle d'accès multi-tenant
*/
@Path("/api/realm-assignments")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@Tag(name = "Realm Assignments", description = "Gestion des affectations de realms (contrôle d'accès multi-tenant)")
@Slf4j
public class RealmAssignmentResource {
@Inject
RealmAuthorizationService realmAuthorizationService;
@Context
SecurityContext securityContext;
// ==================== Endpoints de consultation ====================
@GET
@Operation(summary = "Lister toutes les affectations", description = "Liste toutes les affectations de realms")
@APIResponses({
@APIResponse(responseCode = "200", description = "Liste des affectations"),
@APIResponse(responseCode = "500", description = "Erreur serveur")
})
@RolesAllowed({"admin"})
public Response getAllAssignments() {
log.info("GET /api/realm-assignments - Récupération de toutes les affectations");
try {
List<RealmAssignmentDTO> assignments = realmAuthorizationService.getAllAssignments();
return Response.ok(assignments).build();
} catch (Exception e) {
log.error("Erreur lors de la récupération des affectations", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(new ErrorResponse(e.getMessage()))
.build();
}
}
@GET
@Path("/user/{userId}")
@Operation(summary = "Affectations par utilisateur", description = "Liste les realms assignés à un utilisateur")
@APIResponses({
@APIResponse(responseCode = "200", description = "Liste des affectations"),
@APIResponse(responseCode = "500", description = "Erreur serveur")
})
@RolesAllowed({"admin", "user_manager"})
public Response getAssignmentsByUser(
@Parameter(description = "ID de l'utilisateur") @PathParam("userId") @NotBlank String userId
) {
log.info("GET /api/realm-assignments/user/{}", userId);
try {
List<RealmAssignmentDTO> assignments = realmAuthorizationService.getAssignmentsByUser(userId);
return Response.ok(assignments).build();
} catch (Exception e) {
log.error("Erreur lors de la récupération des affectations pour l'utilisateur {}", userId, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(new ErrorResponse(e.getMessage()))
.build();
}
}
@GET
@Path("/realm/{realmName}")
@Operation(summary = "Affectations par realm", description = "Liste les utilisateurs ayant accès à un realm")
@APIResponses({
@APIResponse(responseCode = "200", description = "Liste des affectations"),
@APIResponse(responseCode = "500", description = "Erreur serveur")
})
@RolesAllowed({"admin"})
public Response getAssignmentsByRealm(
@Parameter(description = "Nom du realm") @PathParam("realmName") @NotBlank String realmName
) {
log.info("GET /api/realm-assignments/realm/{}", realmName);
try {
List<RealmAssignmentDTO> assignments = realmAuthorizationService.getAssignmentsByRealm(realmName);
return Response.ok(assignments).build();
} catch (Exception e) {
log.error("Erreur lors de la récupération des affectations pour le realm {}", realmName, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(new ErrorResponse(e.getMessage()))
.build();
}
}
@GET
@Path("/{assignmentId}")
@Operation(summary = "Récupérer une affectation", description = "Récupère une affectation par son ID")
@APIResponses({
@APIResponse(responseCode = "200", description = "Affectation trouvée"),
@APIResponse(responseCode = "404", description = "Affectation non trouvée"),
@APIResponse(responseCode = "500", description = "Erreur serveur")
})
@RolesAllowed({"admin"})
public Response getAssignmentById(
@Parameter(description = "ID de l'affectation") @PathParam("assignmentId") @NotBlank String assignmentId
) {
log.info("GET /api/realm-assignments/{}", assignmentId);
try {
return realmAuthorizationService.getAssignmentById(assignmentId)
.map(assignment -> Response.ok(assignment).build())
.orElse(Response.status(Response.Status.NOT_FOUND)
.entity(new ErrorResponse("Affectation non trouvée"))
.build());
} catch (Exception e) {
log.error("Erreur lors de la récupération de l'affectation {}", assignmentId, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(new ErrorResponse(e.getMessage()))
.build();
}
}
// ==================== Endpoints de vérification ====================
@GET
@Path("/check")
@Operation(summary = "Vérifier l'accès", description = "Vérifie si un utilisateur peut gérer un realm")
@APIResponses({
@APIResponse(responseCode = "200", description = "Vérification effectuée"),
@APIResponse(responseCode = "500", description = "Erreur serveur")
})
@RolesAllowed({"admin", "user_manager"})
public Response canManageRealm(
@Parameter(description = "ID de l'utilisateur") @QueryParam("userId") @NotBlank String userId,
@Parameter(description = "Nom du realm") @QueryParam("realmName") @NotBlank String realmName
) {
log.info("GET /api/realm-assignments/check - userId: {}, realmName: {}", userId, realmName);
try {
boolean canManage = realmAuthorizationService.canManageRealm(userId, realmName);
return Response.ok(new CheckResponse(canManage, userId, realmName)).build();
} catch (Exception e) {
log.error("Erreur lors de la vérification d'accès", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(new ErrorResponse(e.getMessage()))
.build();
}
}
@GET
@Path("/authorized-realms/{userId}")
@Operation(summary = "Realms autorisés", description = "Liste les realms qu'un utilisateur peut gérer")
@APIResponses({
@APIResponse(responseCode = "200", description = "Liste des realms"),
@APIResponse(responseCode = "500", description = "Erreur serveur")
})
@RolesAllowed({"admin", "user_manager"})
public Response getAuthorizedRealms(
@Parameter(description = "ID de l'utilisateur") @PathParam("userId") @NotBlank String userId
) {
log.info("GET /api/realm-assignments/authorized-realms/{}", userId);
try {
List<String> realms = realmAuthorizationService.getAuthorizedRealms(userId);
boolean isSuperAdmin = realmAuthorizationService.isSuperAdmin(userId);
return Response.ok(new AuthorizedRealmsResponse(realms, isSuperAdmin)).build();
} catch (Exception e) {
log.error("Erreur lors de la récupération des realms autorisés pour {}", userId, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(new ErrorResponse(e.getMessage()))
.build();
}
}
// ==================== Endpoints de modification ====================
@POST
@Operation(summary = "Assigner un realm", description = "Assigne un realm à un utilisateur")
@APIResponses({
@APIResponse(responseCode = "201", description = "Affectation créée",
content = @Content(schema = @Schema(implementation = RealmAssignmentDTO.class))),
@APIResponse(responseCode = "400", description = "Données invalides"),
@APIResponse(responseCode = "409", description = "Affectation existe déjà"),
@APIResponse(responseCode = "500", description = "Erreur serveur")
})
@RolesAllowed({"admin"})
public Response assignRealmToUser(@Valid @NotNull RealmAssignmentDTO assignment) {
log.info("POST /api/realm-assignments - Assignation du realm {} à l'utilisateur {}",
assignment.getRealmName(), assignment.getUserId());
try {
// Ajouter l'utilisateur qui fait l'assignation
if (securityContext.getUserPrincipal() != null) {
assignment.setAssignedBy(securityContext.getUserPrincipal().getName());
}
RealmAssignmentDTO createdAssignment = realmAuthorizationService.assignRealmToUser(assignment);
return Response.status(Response.Status.CREATED).entity(createdAssignment).build();
} catch (IllegalArgumentException e) {
log.warn("Données invalides lors de l'assignation: {}", e.getMessage());
return Response.status(Response.Status.CONFLICT)
.entity(new ErrorResponse(e.getMessage()))
.build();
} catch (Exception e) {
log.error("Erreur lors de l'assignation du realm", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(new ErrorResponse(e.getMessage()))
.build();
}
}
@DELETE
@Path("/user/{userId}/realm/{realmName}")
@Operation(summary = "Révoquer un realm", description = "Retire l'accès d'un utilisateur à un realm")
@APIResponses({
@APIResponse(responseCode = "204", description = "Affectation révoquée"),
@APIResponse(responseCode = "500", description = "Erreur serveur")
})
@RolesAllowed({"admin"})
public Response revokeRealmFromUser(
@Parameter(description = "ID de l'utilisateur") @PathParam("userId") @NotBlank String userId,
@Parameter(description = "Nom du realm") @PathParam("realmName") @NotBlank String realmName
) {
log.info("DELETE /api/realm-assignments/user/{}/realm/{}", userId, realmName);
try {
realmAuthorizationService.revokeRealmFromUser(userId, realmName);
return Response.noContent().build();
} catch (Exception e) {
log.error("Erreur lors de la révocation du realm {} pour {}", realmName, userId, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(new ErrorResponse(e.getMessage()))
.build();
}
}
@DELETE
@Path("/user/{userId}")
@Operation(summary = "Révoquer tous les realms", description = "Retire tous les accès d'un utilisateur")
@APIResponses({
@APIResponse(responseCode = "204", description = "Affectations révoquées"),
@APIResponse(responseCode = "500", description = "Erreur serveur")
})
@RolesAllowed({"admin"})
public Response revokeAllRealmsFromUser(
@Parameter(description = "ID de l'utilisateur") @PathParam("userId") @NotBlank String userId
) {
log.info("DELETE /api/realm-assignments/user/{}", userId);
try {
realmAuthorizationService.revokeAllRealmsFromUser(userId);
return Response.noContent().build();
} catch (Exception e) {
log.error("Erreur lors de la révocation de tous les realms pour {}", userId, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(new ErrorResponse(e.getMessage()))
.build();
}
}
@PUT
@Path("/{assignmentId}/deactivate")
@Operation(summary = "Désactiver une affectation", description = "Désactive une affectation sans la supprimer")
@APIResponses({
@APIResponse(responseCode = "204", description = "Affectation désactivée"),
@APIResponse(responseCode = "404", description = "Affectation non trouvée"),
@APIResponse(responseCode = "500", description = "Erreur serveur")
})
@RolesAllowed({"admin"})
public Response deactivateAssignment(
@Parameter(description = "ID de l'affectation") @PathParam("assignmentId") @NotBlank String assignmentId
) {
log.info("PUT /api/realm-assignments/{}/deactivate", assignmentId);
try {
realmAuthorizationService.deactivateAssignment(assignmentId);
return Response.noContent().build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.NOT_FOUND)
.entity(new ErrorResponse(e.getMessage()))
.build();
} catch (Exception e) {
log.error("Erreur lors de la désactivation de l'affectation {}", assignmentId, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(new ErrorResponse(e.getMessage()))
.build();
}
}
@PUT
@Path("/{assignmentId}/activate")
@Operation(summary = "Activer une affectation", description = "Réactive une affectation")
@APIResponses({
@APIResponse(responseCode = "204", description = "Affectation activée"),
@APIResponse(responseCode = "404", description = "Affectation non trouvée"),
@APIResponse(responseCode = "500", description = "Erreur serveur")
})
@RolesAllowed({"admin"})
public Response activateAssignment(
@Parameter(description = "ID de l'affectation") @PathParam("assignmentId") @NotBlank String assignmentId
) {
log.info("PUT /api/realm-assignments/{}/activate", assignmentId);
try {
realmAuthorizationService.activateAssignment(assignmentId);
return Response.noContent().build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.NOT_FOUND)
.entity(new ErrorResponse(e.getMessage()))
.build();
} catch (Exception e) {
log.error("Erreur lors de l'activation de l'affectation {}", assignmentId, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(new ErrorResponse(e.getMessage()))
.build();
}
}
@PUT
@Path("/super-admin/{userId}")
@Operation(summary = "Définir super admin", description = "Définit ou retire le statut de super admin")
@APIResponses({
@APIResponse(responseCode = "204", description = "Statut modifié"),
@APIResponse(responseCode = "500", description = "Erreur serveur")
})
@RolesAllowed({"admin"})
public Response setSuperAdmin(
@Parameter(description = "ID de l'utilisateur") @PathParam("userId") @NotBlank String userId,
@Parameter(description = "Super admin (true/false)") @QueryParam("superAdmin") @NotNull Boolean superAdmin
) {
log.info("PUT /api/realm-assignments/super-admin/{} - superAdmin: {}", userId, superAdmin);
try {
realmAuthorizationService.setSuperAdmin(userId, superAdmin);
return Response.noContent().build();
} catch (Exception e) {
log.error("Erreur lors de la modification du statut super admin pour {}", userId, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(new ErrorResponse(e.getMessage()))
.build();
}
}
// ==================== Classes internes pour les réponses ====================
@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;
}
}
@Schema(description = "Réponse de vérification d'accès")
public static class CheckResponse {
@Schema(description = "L'utilisateur peut gérer le realm")
public boolean canManage;
@Schema(description = "ID de l'utilisateur")
public String userId;
@Schema(description = "Nom du realm")
public String realmName;
public CheckResponse(boolean canManage, String userId, String realmName) {
this.canManage = canManage;
this.userId = userId;
this.realmName = realmName;
}
}
@Schema(description = "Réponse des realms autorisés")
public static class AuthorizedRealmsResponse {
@Schema(description = "Liste des realms (vide si super admin)")
public List<String> realms;
@Schema(description = "L'utilisateur est super admin")
public boolean isSuperAdmin;
public AuthorizedRealmsResponse(List<String> realms, boolean isSuperAdmin) {
this.realms = realms;
this.isSuperAdmin = isSuperAdmin;
}
}
}

View File

@@ -0,0 +1,77 @@
package dev.lions.user.manager.resource;
import dev.lions.user.manager.client.KeycloakAdminClient;
import io.quarkus.security.identity.SecurityIdentity;
import jakarta.annotation.security.RolesAllowed;
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 jakarta.ws.rs.core.Response;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.microprofile.openapi.annotations.Operation;
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;
/**
* Ressource REST pour la gestion des realms Keycloak
*/
@Path("/api/realms")
@Tag(name = "Realms", description = "Gestion des realms Keycloak")
@Slf4j
public class RealmResource {
@Inject
KeycloakAdminClient keycloakAdminClient;
@Inject
SecurityIdentity securityIdentity;
@GET
@Path("/list")
@Produces(MediaType.APPLICATION_JSON)
@Operation(summary = "Lister tous les realms", description = "Récupère la liste de tous les realms disponibles dans Keycloak")
@APIResponses({
@APIResponse(responseCode = "200", description = "Liste des realms"),
@APIResponse(responseCode = "500", description = "Erreur serveur")
})
@RolesAllowed({"admin", "user_manager", "user_viewer", "role_manager", "role_viewer"})
public Response getAllRealms() {
log.info("GET /api/realms/list");
try {
List<String> realms = keycloakAdminClient.getAllRealms();
log.info("Récupération réussie: {} realms trouvés", realms.size());
return Response.ok(realms).build();
} catch (Exception e) {
log.error("Erreur lors de la récupération des realms", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(new ErrorResponse("Erreur lors de la récupération des realms: " + e.getMessage()))
.build();
}
}
/**
* Classe interne pour les réponses d'erreur
*/
public static class ErrorResponse {
private String message;
public ErrorResponse(String message) {
this.message = message;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}
}

View File

@@ -109,6 +109,7 @@ public class RoleResource {
@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 = "400", description = "Realm invalide ou inexistant"),
@APIResponse(responseCode = "500", description = "Erreur serveur")
})
@RolesAllowed({"admin", "role_manager", "role_viewer"})
@@ -120,6 +121,11 @@ public class RoleResource {
try {
List<RoleDTO> roles = roleService.getAllRealmRoles(realmName);
return Response.ok(roles).build();
} catch (IllegalArgumentException e) {
log.warn("Realm invalide ou inexistant: {}", e.getMessage());
return Response.status(Response.Status.BAD_REQUEST)
.entity(new ErrorResponse(e.getMessage()))
.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)
@@ -224,7 +230,7 @@ public class RoleResource {
clientId, realmName);
try {
RoleDTO createdRole = roleService.createClientRole(roleDTO, clientId, realmName);
RoleDTO createdRole = roleService.createClientRole(roleDTO, realmName, clientId);
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());

View File

@@ -4,6 +4,7 @@ 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.PermitAll;
import jakarta.annotation.security.RolesAllowed;
import jakarta.inject.Inject;
import jakarta.validation.Valid;
@@ -31,6 +32,7 @@ import java.util.List;
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@Tag(name = "Users", description = "Gestion des utilisateurs Keycloak")
@PermitAll // DEV: Permet l'accès sans authentification (écrasé par @RolesAllowed sur les méthodes en PROD)
@Slf4j
public class UserResource {
@@ -162,12 +164,44 @@ public class UserResource {
@RolesAllowed({"admin", "user_manager"})
public Response updateUser(
@PathParam("userId") @NotBlank String userId,
@Valid @NotNull UserDTO user,
@NotNull UserDTO user,
@QueryParam("realm") @NotBlank String realmName
) {
log.info("PUT /api/users/{} - Mise à jour", userId);
try {
// Validation manuelle des champs obligatoires
if (user.getPrenom() == null || user.getPrenom().trim().isEmpty()) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(new ErrorResponse("Le prénom est obligatoire"))
.build();
}
if (user.getPrenom().length() < 2 || user.getPrenom().length() > 100) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(new ErrorResponse("Le prénom doit contenir entre 2 et 100 caractères"))
.build();
}
if (user.getNom() == null || user.getNom().trim().isEmpty()) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(new ErrorResponse("Le nom est obligatoire"))
.build();
}
if (user.getNom().length() < 2 || user.getNom().length() > 100) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(new ErrorResponse("Le nom doit contenir entre 2 et 100 caractères"))
.build();
}
if (user.getEmail() == null || user.getEmail().trim().isEmpty()) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(new ErrorResponse("L'email est obligatoire"))
.build();
}
if (!user.getEmail().matches("^[A-Za-z0-9+_.-]+@(.+)$")) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(new ErrorResponse("Format d'email invalide"))
.build();
}
UserDTO updatedUser = userService.updateUser(userId, user, realmName);
return Response.ok(updatedUser).build();
} catch (RuntimeException e) {

View File

@@ -0,0 +1,36 @@
package dev.lions.user.manager.security;
import io.quarkus.security.identity.AuthenticationRequestContext;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.security.identity.SecurityIdentityAugmentor;
import io.quarkus.security.runtime.QuarkusSecurityIdentity;
import io.smallrye.mutiny.Uni;
import jakarta.enterprise.context.ApplicationScoped;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import java.util.Set;
/**
* Augmenteur de sécurité pour le mode DEV
* Ajoute automatiquement les rôles admin et user_manager à toutes les requêtes
* Permet de tester l'API sans authentification Keycloak
*/
@ApplicationScoped
public class DevModeSecurityAugmentor implements SecurityIdentityAugmentor {
@ConfigProperty(name = "quarkus.oidc.enabled", defaultValue = "true")
boolean oidcEnabled;
@Override
public Uni<SecurityIdentity> augment(SecurityIdentity identity, AuthenticationRequestContext context) {
// Seulement actif si OIDC est désactivé (mode DEV)
if (!oidcEnabled && identity.isAnonymous()) {
// Créer une identité avec les rôles nécessaires pour DEV
return Uni.createFrom().item(QuarkusSecurityIdentity.builder(identity)
.setPrincipal(() -> "dev-user")
.addRoles(Set.of("admin", "user_manager", "user_viewer"))
.build());
}
return Uni.createFrom().item(identity);
}
}

View File

@@ -18,7 +18,7 @@ import java.security.Principal;
* En prod, laisse le SecurityContext réel de Quarkus
*/
@Provider
@Priority(Priorities.AUTHENTICATION - 1) // S'exécute avant l'authentification
@Priority(Priorities.AUTHENTICATION - 10) // S'exécute très tôt, avant l'authentification
public class DevSecurityContextProducer implements ContainerRequestFilter {
private static final Logger LOG = Logger.getLogger(DevSecurityContextProducer.class);
@@ -27,13 +27,27 @@ public class DevSecurityContextProducer implements ContainerRequestFilter {
@ConfigProperty(name = "quarkus.profile", defaultValue = "prod")
String profile;
@Inject
@ConfigProperty(name = "quarkus.oidc.enabled", defaultValue = "true")
boolean oidcEnabled;
@Override
public void filter(ContainerRequestContext requestContext) {
// En dev, remplacer le SecurityContext par un mock
if ("dev".equals(profile) || "development".equals(profile)) {
LOG.debug("Mode dev: remplacement du SecurityContext par un mock avec tous les rôles");
// Détecter le mode dev : si OIDC est désactivé, on est probablement en dev
// ou si le profil est explicitement "dev" ou "development"
boolean isDevMode = !oidcEnabled || "dev".equals(profile) || "development".equals(profile);
if (isDevMode) {
String path = requestContext.getUriInfo().getPath();
LOG.infof("Mode dev détecté (profile=%s, oidc.enabled=%s): remplacement du SecurityContext pour le chemin %s",
profile, oidcEnabled, path);
SecurityContext original = requestContext.getSecurityContext();
requestContext.setSecurityContext(new DevSecurityContext(original));
LOG.debugf("SecurityContext remplacé - isUserInRole('admin')=%s, isUserInRole('user_manager')=%s",
new DevSecurityContext(original).isUserInRole("admin"),
new DevSecurityContext(original).isUserInRole("user_manager"));
} else {
LOG.debugf("Mode prod - SecurityContext original conservé (profile=%s, oidc.enabled=%s)", profile, oidcEnabled);
}
}

View File

@@ -0,0 +1,72 @@
package dev.lions.user.manager.service.exception;
/**
* Exception levée lorsqu'une erreur survient lors de l'appel au service Keycloak.
*
* @author Lions User Manager Team
* @version 1.0
*/
public class KeycloakServiceException extends RuntimeException {
private final int httpStatus;
private final String serviceName;
public KeycloakServiceException(String message) {
super(message);
this.httpStatus = 0;
this.serviceName = "Keycloak";
}
public KeycloakServiceException(String message, Throwable cause) {
super(message, cause);
this.httpStatus = 0;
this.serviceName = "Keycloak";
}
public KeycloakServiceException(String message, int httpStatus) {
super(message);
this.httpStatus = httpStatus;
this.serviceName = "Keycloak";
}
public KeycloakServiceException(String message, int httpStatus, Throwable cause) {
super(message, cause);
this.httpStatus = httpStatus;
this.serviceName = "Keycloak";
}
public int getHttpStatus() {
return httpStatus;
}
public String getServiceName() {
return serviceName;
}
/**
* Exception spécifique pour les erreurs de connexion (service indisponible)
*/
public static class ServiceUnavailableException extends KeycloakServiceException {
public ServiceUnavailableException(String message) {
super("Service Keycloak indisponible: " + message);
}
public ServiceUnavailableException(String message, Throwable cause) {
super("Service Keycloak indisponible: " + message, cause);
}
}
/**
* Exception spécifique pour les erreurs de timeout
*/
public static class TimeoutException extends KeycloakServiceException {
public TimeoutException(String message) {
super("Timeout lors de l'appel au service Keycloak: " + message);
}
public TimeoutException(String message, Throwable cause) {
super("Timeout lors de l'appel au service Keycloak: " + message, cause);
}
}
}

View File

@@ -0,0 +1,346 @@
package dev.lions.user.manager.service.impl;
import dev.lions.user.manager.dto.realm.RealmAssignmentDTO;
import dev.lions.user.manager.enums.audit.TypeActionAudit;
import dev.lions.user.manager.service.AuditService;
import dev.lions.user.manager.service.RealmAuthorizationService;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.extern.slf4j.Slf4j;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
/**
* Implémentation du service d'autorisation multi-tenant par realm
*
* NOTE: Cette implémentation utilise un stockage en mémoire (ConcurrentHashMap)
* Pour la production, migrer vers une base de données PostgreSQL
*/
@ApplicationScoped
@Slf4j
public class RealmAuthorizationServiceImpl implements RealmAuthorizationService {
@Inject
AuditService auditService;
// Stockage temporaire en mémoire (à remplacer par BD en production)
private final Map<String, RealmAssignmentDTO> assignmentsById = new ConcurrentHashMap<>();
private final Map<String, Set<String>> userToRealms = new ConcurrentHashMap<>();
private final Map<String, Set<String>> realmToUsers = new ConcurrentHashMap<>();
private final Set<String> superAdmins = ConcurrentHashMap.newKeySet();
@Override
public List<RealmAssignmentDTO> getAllAssignments() {
log.debug("Récupération de toutes les assignations de realms");
return new ArrayList<>(assignmentsById.values());
}
@Override
public List<RealmAssignmentDTO> getAssignmentsByUser(@NotBlank String userId) {
log.debug("Récupération des assignations pour l'utilisateur: {}", userId);
return assignmentsById.values().stream()
.filter(assignment -> assignment.getUserId().equals(userId))
.filter(RealmAssignmentDTO::isActive)
.filter(assignment -> !assignment.isExpired())
.collect(Collectors.toList());
}
@Override
public List<RealmAssignmentDTO> getAssignmentsByRealm(@NotBlank String realmName) {
log.debug("Récupération des assignations pour le realm: {}", realmName);
return assignmentsById.values().stream()
.filter(assignment -> assignment.getRealmName().equals(realmName))
.filter(RealmAssignmentDTO::isActive)
.filter(assignment -> !assignment.isExpired())
.collect(Collectors.toList());
}
@Override
public Optional<RealmAssignmentDTO> getAssignmentById(@NotBlank String assignmentId) {
log.debug("Récupération de l'assignation: {}", assignmentId);
return Optional.ofNullable(assignmentsById.get(assignmentId));
}
@Override
public boolean canManageRealm(@NotBlank String userId, @NotBlank String realmName) {
log.debug("Vérification si {} peut gérer le realm {}", userId, realmName);
// Super admin peut tout gérer
if (isSuperAdmin(userId)) {
return true;
}
// Vérifier les assignations actives et non expirées
return assignmentsById.values().stream()
.anyMatch(assignment ->
assignment.getUserId().equals(userId) &&
assignment.getRealmName().equals(realmName) &&
assignment.isActive() &&
!assignment.isExpired()
);
}
@Override
public boolean isSuperAdmin(@NotBlank String userId) {
return superAdmins.contains(userId);
}
@Override
public List<String> getAuthorizedRealms(@NotBlank String userId) {
log.debug("Récupération des realms autorisés pour: {}", userId);
// Super admin retourne liste vide (convention: peut tout gérer)
if (isSuperAdmin(userId)) {
return Collections.emptyList();
}
// Retourner les realms assignés actifs et non expirés
return assignmentsById.values().stream()
.filter(assignment -> assignment.getUserId().equals(userId))
.filter(RealmAssignmentDTO::isActive)
.filter(assignment -> !assignment.isExpired())
.map(RealmAssignmentDTO::getRealmName)
.distinct()
.collect(Collectors.toList());
}
@Override
public RealmAssignmentDTO assignRealmToUser(@Valid @NotNull RealmAssignmentDTO assignment) {
log.info("Assignation du realm {} à l'utilisateur {}",
assignment.getRealmName(), assignment.getUserId());
// Validation
if (assignment.getUserId() == null || assignment.getUserId().isBlank()) {
throw new IllegalArgumentException("L'ID utilisateur est obligatoire");
}
if (assignment.getRealmName() == null || assignment.getRealmName().isBlank()) {
throw new IllegalArgumentException("Le nom du realm est obligatoire");
}
// Vérifier si l'assignation existe déjà
if (assignmentExists(assignment.getUserId(), assignment.getRealmName())) {
throw new IllegalArgumentException(
String.format("L'utilisateur %s a déjà accès au realm %s",
assignment.getUserId(), assignment.getRealmName())
);
}
// Générer ID si absent
if (assignment.getId() == null) {
assignment.setId(UUID.randomUUID().toString());
}
// Compléter les métadonnées
assignment.setAssignedAt(LocalDateTime.now());
assignment.setActive(true);
assignment.setDateCreation(LocalDateTime.now());
// Stocker l'assignation
assignmentsById.put(assignment.getId(), assignment);
// Mettre à jour les index
userToRealms.computeIfAbsent(assignment.getUserId(), k -> ConcurrentHashMap.newKeySet())
.add(assignment.getRealmName());
realmToUsers.computeIfAbsent(assignment.getRealmName(), k -> ConcurrentHashMap.newKeySet())
.add(assignment.getUserId());
// Audit
auditService.logSuccess(
TypeActionAudit.REALM_ASSIGN,
"REALM_ASSIGNMENT",
assignment.getId(),
assignment.getUsername(),
assignment.getRealmName(),
assignment.getAssignedBy() != null ? assignment.getAssignedBy() : "system",
String.format("Assignation du realm %s à %s", assignment.getRealmName(), assignment.getUsername())
);
log.info("Realm {} assigné avec succès à {}", assignment.getRealmName(), assignment.getUserId());
return assignment;
}
@Override
public void revokeRealmFromUser(@NotBlank String userId, @NotBlank String realmName) {
log.info("Révocation du realm {} pour l'utilisateur {}", realmName, userId);
// Trouver et supprimer l'assignation
Optional<RealmAssignmentDTO> assignment = assignmentsById.values().stream()
.filter(a -> a.getUserId().equals(userId) && a.getRealmName().equals(realmName))
.findFirst();
if (assignment.isEmpty()) {
log.warn("Aucune assignation trouvée pour {} / {}", userId, realmName);
return;
}
RealmAssignmentDTO assignmentToRemove = assignment.get();
assignmentsById.remove(assignmentToRemove.getId());
// Mettre à jour les index
Set<String> realms = userToRealms.get(userId);
if (realms != null) {
realms.remove(realmName);
if (realms.isEmpty()) {
userToRealms.remove(userId);
}
}
Set<String> users = realmToUsers.get(realmName);
if (users != null) {
users.remove(userId);
if (users.isEmpty()) {
realmToUsers.remove(realmName);
}
}
// Audit
auditService.logSuccess(
TypeActionAudit.REALM_REVOKE,
"REALM_ASSIGNMENT",
assignmentToRemove.getId(),
assignmentToRemove.getUsername(),
realmName,
"system",
String.format("Révocation du realm %s pour %s", realmName, assignmentToRemove.getUsername())
);
log.info("Realm {} révoqué avec succès pour {}", realmName, userId);
}
@Override
public void revokeAllRealmsFromUser(@NotBlank String userId) {
log.info("Révocation de tous les realms pour l'utilisateur {}", userId);
List<RealmAssignmentDTO> userAssignments = getAssignmentsByUser(userId);
userAssignments.forEach(assignment ->
revokeRealmFromUser(userId, assignment.getRealmName())
);
log.info("{} realm(s) révoqué(s) pour {}", userAssignments.size(), userId);
}
@Override
public void revokeAllUsersFromRealm(@NotBlank String realmName) {
log.info("Révocation de tous les utilisateurs du realm {}", realmName);
List<RealmAssignmentDTO> realmAssignments = getAssignmentsByRealm(realmName);
realmAssignments.forEach(assignment ->
revokeRealmFromUser(assignment.getUserId(), realmName)
);
log.info("{} utilisateur(s) révoqué(s) du realm {}", realmAssignments.size(), realmName);
}
@Override
public void setSuperAdmin(@NotBlank String userId, boolean superAdmin) {
log.info("Définition de {} comme super admin: {}", userId, superAdmin);
if (superAdmin) {
superAdmins.add(userId);
auditService.logSuccess(
TypeActionAudit.REALM_SET_SUPER_ADMIN,
"USER",
userId,
userId,
"lions-user-manager",
"system",
String.format("Utilisateur %s défini comme super admin", userId)
);
} else {
superAdmins.remove(userId);
auditService.logSuccess(
TypeActionAudit.REALM_SET_SUPER_ADMIN,
"USER",
userId,
userId,
"lions-user-manager",
"system",
String.format("Privilèges super admin retirés pour %s", userId)
);
}
}
@Override
public void deactivateAssignment(@NotBlank String assignmentId) {
log.info("Désactivation de l'assignation {}", assignmentId);
RealmAssignmentDTO assignment = assignmentsById.get(assignmentId);
if (assignment == null) {
throw new IllegalArgumentException("Assignation non trouvée: " + assignmentId);
}
assignment.setActive(false);
assignment.setDateModification(LocalDateTime.now());
auditService.logSuccess(
TypeActionAudit.REALM_DEACTIVATE,
"REALM_ASSIGNMENT",
assignment.getId(),
assignment.getUsername(),
assignment.getRealmName(),
"system",
String.format("Désactivation de l'assignation %s", assignmentId)
);
}
@Override
public void activateAssignment(@NotBlank String assignmentId) {
log.info("Activation de l'assignation {}", assignmentId);
RealmAssignmentDTO assignment = assignmentsById.get(assignmentId);
if (assignment == null) {
throw new IllegalArgumentException("Assignation non trouvée: " + assignmentId);
}
assignment.setActive(true);
assignment.setDateModification(LocalDateTime.now());
auditService.logSuccess(
TypeActionAudit.REALM_ACTIVATE,
"REALM_ASSIGNMENT",
assignment.getId(),
assignment.getUsername(),
assignment.getRealmName(),
"system",
String.format("Activation de l'assignation %s", assignmentId)
);
}
@Override
public long countAssignmentsByUser(@NotBlank String userId) {
return assignmentsById.values().stream()
.filter(assignment -> assignment.getUserId().equals(userId))
.filter(RealmAssignmentDTO::isActive)
.filter(assignment -> !assignment.isExpired())
.count();
}
@Override
public long countUsersByRealm(@NotBlank String realmName) {
return assignmentsById.values().stream()
.filter(assignment -> assignment.getRealmName().equals(realmName))
.filter(RealmAssignmentDTO::isActive)
.filter(assignment -> !assignment.isExpired())
.map(RealmAssignmentDTO::getUserId)
.distinct()
.count();
}
@Override
public boolean assignmentExists(@NotBlank String userId, @NotBlank String realmName) {
return assignmentsById.values().stream()
.anyMatch(assignment ->
assignment.getUserId().equals(userId) &&
assignment.getRealmName().equals(realmName) &&
assignment.isActive()
);
}
}

View File

@@ -221,7 +221,7 @@ public class RoleServiceImpl implements RoleService {
try {
// Vérifier que le realm existe
if (!keycloakAdminClient.realmExists(realmName)) {
log.error("Le realm {} n'existe pas", realmName);
log.warn("Le realm {} n'existe pas", realmName);
throw new IllegalArgumentException("Le realm '" + realmName + "' n'existe pas");
}
@@ -232,7 +232,19 @@ public class RoleServiceImpl implements RoleService {
log.info("Récupération réussie: {} rôles trouvés dans le realm {}", roleReps.size(), realmName);
return RoleMapper.toDTOList(roleReps, realmName, TypeRole.REALM_ROLE);
} catch (NotFoundException e) {
log.warn("Realm {} non trouvé (404): {}", realmName, e.getMessage());
throw new IllegalArgumentException("Le realm '" + realmName + "' n'existe pas", e);
} catch (Exception e) {
// Vérifier si c'est une erreur 404 dans le message
String errorMessage = e.getMessage();
if (errorMessage != null && (errorMessage.contains("404") ||
errorMessage.contains("Server response is: 404") ||
errorMessage.contains("Not Found"))) {
log.warn("Realm {} non trouvé (404 détecté dans l'erreur): {}", realmName, errorMessage);
throw new IllegalArgumentException("Le realm '" + realmName + "' n'existe pas", e);
}
log.error("Erreur lors de la récupération des rôles realm du realm {}: {}", realmName, e.getMessage(), e);
throw new RuntimeException("Erreur lors de la récupération des rôles realm: " + e.getMessage(), e);
}

View File

@@ -6,18 +6,23 @@ 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 dev.lions.user.manager.service.exception.KeycloakServiceException;
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 jakarta.ws.rs.core.Response;
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.RoleRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import java.net.ConnectException;
import java.net.SocketTimeoutException;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
@@ -48,30 +53,28 @@ public class UserServiceImpl implements UserService {
if (criteria.getSearchTerm() != null && !criteria.getSearchTerm().isBlank()) {
// Recherche globale
users = usersResource.search(
criteria.getSearchTerm(),
criteria.getOffset(),
criteria.getPageSize()
);
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
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
criteria.getEmail(),
true // exact match
);
} else {
// Liste tous les utilisateurs
users = usersResource.list(
criteria.getOffset(),
criteria.getPageSize()
);
criteria.getOffset(),
criteria.getPageSize());
}
// Filtrer selon les critères supplémentaires
@@ -88,7 +91,8 @@ public class UserServiceImpl implements UserService {
} catch (Exception e) {
log.error("Erreur lors de la recherche d'utilisateurs", e);
throw new RuntimeException("Impossible de rechercher les utilisateurs", e);
handleConnectionException(e, "recherche d'utilisateurs");
return null; // Ne sera jamais atteint car handleConnectionException lance une exception
}
}
@@ -99,13 +103,40 @@ public class UserServiceImpl implements UserService {
try {
UserResource userResource = keycloakAdminClient.getUsers(realmName).get(userId);
UserRepresentation userRep = userResource.toRepresentation();
return Optional.of(UserMapper.toDTO(userRep, realmName));
UserDTO userDTO = UserMapper.toDTO(userRep, realmName);
// Récupérer les rôles realm de l'utilisateur
try {
List<RoleRepresentation> realmRoles = userResource.roles().realmLevel().listAll();
if (realmRoles != null && !realmRoles.isEmpty()) {
List<String> roleNames = realmRoles.stream()
.map(RoleRepresentation::getName)
.collect(Collectors.toList());
userDTO.setRealmRoles(roleNames);
}
} catch (Exception e) {
log.warn("Erreur lors de la récupération des rôles realm pour l'utilisateur {}: {}", userId,
e.getMessage());
// Ne pas échouer si les rôles ne peuvent pas être récupérés
}
return Optional.of(userDTO);
} catch (NotFoundException e) {
log.warn("Utilisateur {} non trouvé dans le realm {}", userId, realmName);
return Optional.empty();
} catch (Exception e) {
// Vérifier si l'exception contient un message indiquant un 404
String errorMessage = e.getMessage();
if (errorMessage != null && (errorMessage.contains("404") ||
errorMessage.contains("Server response is: 404") ||
errorMessage.contains("Received: 'Server response is: 404'"))) {
log.warn("Utilisateur {} non trouvé dans le realm {} (404 détecté dans l'exception)", userId,
realmName);
return Optional.empty();
}
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);
handleConnectionException(e, "récupération de l'utilisateur " + userId);
return Optional.empty(); // Ne sera jamais atteint mais nécessaire pour le compilateur
}
}
@@ -115,7 +146,12 @@ public class UserServiceImpl implements UserService {
try {
List<UserRepresentation> users = keycloakAdminClient.getUsers(realmName)
.search(username, 0, 1, true);
.search(username, 0, 1, true);
if (users == null) {
log.warn("Liste d'utilisateurs null retournée pour username {} dans le realm {}", username, realmName);
return Optional.empty();
}
if (users.isEmpty()) {
return Optional.empty();
@@ -124,7 +160,8 @@ public class UserServiceImpl implements UserService {
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);
handleConnectionException(e, "récupération de l'utilisateur par username " + username);
return Optional.empty(); // Ne sera jamais atteint mais nécessaire pour le compilateur
}
}
@@ -134,7 +171,12 @@ public class UserServiceImpl implements UserService {
try {
List<UserRepresentation> users = keycloakAdminClient.getUsers(realmName)
.searchByEmail(email, true);
.searchByEmail(email, true);
if (users == null) {
log.warn("Liste d'utilisateurs null retournée pour email {} dans le realm {}", email, realmName);
return Optional.empty();
}
if (users.isEmpty()) {
return Optional.empty();
@@ -143,7 +185,8 @@ public class UserServiceImpl implements UserService {
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);
handleConnectionException(e, "récupération de l'utilisateur par email " + email);
return Optional.empty(); // Ne sera jamais atteint mais nécessaire pour le compilateur
}
}
@@ -166,10 +209,39 @@ public class UserServiceImpl implements UserService {
// Créer l'utilisateur
UsersResource usersResource = keycloakAdminClient.getUsers(realmName);
var response = usersResource.create(userRep);
Response response = usersResource.create(userRep);
if (response.getStatus() != 201) {
throw new RuntimeException("Échec de la création de l'utilisateur: " + response.getStatusInfo());
// Vérifier si la réponse est null (erreur de connexion)
if (response == null) {
log.error("❌ Réponse null lors de la création de l'utilisateur {} - Service Keycloak indisponible", user.getUsername());
throw new KeycloakServiceException.ServiceUnavailableException(
"Impossible de se connecter au service Keycloak pour créer l'utilisateur: " + user.getUsername());
}
// Vérifier le code de statut HTTP
int status = response.getStatus();
if (status != Response.Status.CREATED.getStatusCode()) {
String errorMessage = "Échec de la création de l'utilisateur";
if (response.getStatusInfo() != null) {
errorMessage += ": " + response.getStatusInfo();
}
// Gérer les différents codes d'erreur HTTP
if (status == 400) {
throw new KeycloakServiceException("Données invalides pour la création de l'utilisateur: " + errorMessage, status);
} else if (status == 409) {
throw new IllegalArgumentException("L'utilisateur existe déjà (conflit détecté par Keycloak)");
} else if (status == 503 || status == 502) {
throw new KeycloakServiceException.ServiceUnavailableException("Service Keycloak indisponible: " + errorMessage);
} else {
throw new KeycloakServiceException(errorMessage, status);
}
}
// Vérifier que la location est présente
if (response.getLocation() == null) {
log.error("❌ Location manquante dans la réponse de création pour l'utilisateur {}", user.getUsername());
throw new KeycloakServiceException("Réponse invalide du service Keycloak: location manquante", status);
}
// Récupérer l'ID de l'utilisateur créé
@@ -178,7 +250,7 @@ public class UserServiceImpl implements UserService {
// Définir le mot de passe si fourni
if (user.getTemporaryPassword() != null) {
setPassword(userId, realmName, user.getTemporaryPassword(),
user.getTemporaryPasswordFlag() != null && user.getTemporaryPasswordFlag());
user.getTemporaryPasswordFlag() != null && user.getTemporaryPasswordFlag());
}
// Récupérer l'utilisateur créé
@@ -188,9 +260,16 @@ public class UserServiceImpl implements UserService {
log.info("✅ Utilisateur créé avec succès: {} (ID: {})", user.getUsername(), userId);
return UserMapper.toDTO(createdUser, realmName);
} catch (IllegalArgumentException e) {
// Répercuter les erreurs de validation
throw e;
} catch (KeycloakServiceException e) {
// Répercuter les erreurs de service
throw e;
} 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);
handleConnectionException(e, "création de l'utilisateur " + user.getUsername());
return null; // Ne sera jamais atteint car handleConnectionException lance une exception
}
}
@@ -201,8 +280,21 @@ public class UserServiceImpl implements UserService {
try {
UserResource userResource = keycloakAdminClient.getUsers(realmName).get(userId);
// Vérifier si userResource est null (peut arriver si la connexion échoue)
if (userResource == null) {
log.error("❌ UserResource null pour l'utilisateur {} - Service Keycloak indisponible", userId);
throw new KeycloakServiceException.ServiceUnavailableException(
"Impossible de se connecter au service Keycloak pour mettre à jour l'utilisateur: " + userId);
}
// Récupérer l'utilisateur existant
UserRepresentation existingUser = userResource.toRepresentation();
if (existingUser == null) {
log.error("❌ UserRepresentation null pour l'utilisateur {} - Service Keycloak indisponible", userId);
throw new KeycloakServiceException.ServiceUnavailableException(
"Impossible de récupérer les données de l'utilisateur depuis Keycloak: " + userId);
}
// Mettre à jour les champs
if (user.getEmail() != null) {
@@ -229,16 +321,24 @@ public class UserServiceImpl implements UserService {
// Récupérer l'utilisateur mis à jour
UserRepresentation updatedUser = userResource.toRepresentation();
if (updatedUser == null) {
log.error("❌ UserRepresentation null après mise à jour pour l'utilisateur {}", userId);
throw new KeycloakServiceException("Impossible de récupérer l'utilisateur mis à jour", 500);
}
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);
throw new KeycloakServiceException("Utilisateur non trouvé: " + userId, 404, e);
} catch (KeycloakServiceException e) {
throw 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);
handleConnectionException(e, "mise à jour de l'utilisateur " + userId);
return null; // Ne sera jamais atteint car handleConnectionException lance une exception
}
}
@@ -248,6 +348,12 @@ public class UserServiceImpl implements UserService {
try {
UserResource userResource = keycloakAdminClient.getUsers(realmName).get(userId);
if (userResource == null) {
log.error("❌ UserResource null pour l'utilisateur {} - Service Keycloak indisponible", userId);
throw new KeycloakServiceException.ServiceUnavailableException(
"Impossible de se connecter au service Keycloak pour supprimer l'utilisateur: " + userId);
}
if (hardDelete) {
// Suppression définitive
@@ -256,6 +362,11 @@ public class UserServiceImpl implements UserService {
} else {
// Soft delete: désactiver l'utilisateur
UserRepresentation user = userResource.toRepresentation();
if (user == null) {
log.error("❌ UserRepresentation null pour l'utilisateur {} - Service Keycloak indisponible", userId);
throw new KeycloakServiceException.ServiceUnavailableException(
"Impossible de récupérer les données de l'utilisateur depuis Keycloak: " + userId);
}
user.setEnabled(false);
userResource.update(user);
log.info("✅ Utilisateur désactivé (soft delete): {}", userId);
@@ -263,10 +374,12 @@ public class UserServiceImpl implements UserService {
} catch (NotFoundException e) {
log.error("❌ Utilisateur {} non trouvé", userId);
throw new RuntimeException("Utilisateur non trouvé", e);
throw new KeycloakServiceException("Utilisateur non trouvé: " + userId, 404, e);
} catch (KeycloakServiceException e) {
throw 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);
handleConnectionException(e, "suppression de l'utilisateur " + userId);
}
}
@@ -276,14 +389,30 @@ public class UserServiceImpl implements UserService {
try {
UserResource userResource = keycloakAdminClient.getUsers(realmName).get(userId);
if (userResource == null) {
log.error("❌ UserResource null pour l'utilisateur {} - Service Keycloak indisponible", userId);
throw new KeycloakServiceException.ServiceUnavailableException(
"Impossible de se connecter au service Keycloak pour activer l'utilisateur: " + userId);
}
UserRepresentation user = userResource.toRepresentation();
if (user == null) {
log.error("❌ UserRepresentation null pour l'utilisateur {} - Service Keycloak indisponible", userId);
throw new KeycloakServiceException.ServiceUnavailableException(
"Impossible de récupérer les données de l'utilisateur depuis Keycloak: " + userId);
}
user.setEnabled(true);
userResource.update(user);
log.info("✅ Utilisateur activé: {}", userId);
} catch (KeycloakServiceException e) {
throw e;
} catch (Exception e) {
log.error("❌ Erreur lors de l'activation de l'utilisateur {}", userId, e);
throw new RuntimeException("Impossible d'activer l'utilisateur", e);
handleConnectionException(e, "activation de l'utilisateur " + userId);
}
}
@@ -293,21 +422,37 @@ public class UserServiceImpl implements UserService {
try {
UserResource userResource = keycloakAdminClient.getUsers(realmName).get(userId);
if (userResource == null) {
log.error("❌ UserResource null pour l'utilisateur {} - Service Keycloak indisponible", userId);
throw new KeycloakServiceException.ServiceUnavailableException(
"Impossible de se connecter au service Keycloak pour désactiver l'utilisateur: " + userId);
}
UserRepresentation user = userResource.toRepresentation();
if (user == null) {
log.error("❌ UserRepresentation null pour l'utilisateur {} - Service Keycloak indisponible", userId);
throw new KeycloakServiceException.ServiceUnavailableException(
"Impossible de récupérer les données de l'utilisateur depuis Keycloak: " + userId);
}
user.setEnabled(false);
userResource.update(user);
log.info("✅ Utilisateur désactivé: {}", userId);
} catch (KeycloakServiceException e) {
throw e;
} 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);
handleConnectionException(e, "désactivation de l'utilisateur " + userId);
}
}
@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);
userId, realmName, raison, duree);
deactivateUser(userId, realmName, raison);
}
@@ -320,7 +465,7 @@ public class UserServiceImpl implements UserService {
@Override
public void resetPassword(@NotBlank String userId, @NotBlank String realmName,
@NotBlank String temporaryPassword, boolean temporary) {
@NotBlank String temporaryPassword, boolean temporary) {
log.info("Réinitialisation du mot de passe pour l'utilisateur {} (temporaire: {})", userId, temporary);
setPassword(userId, realmName, temporaryPassword, temporary);
@@ -332,12 +477,21 @@ public class UserServiceImpl implements UserService {
try {
UserResource userResource = keycloakAdminClient.getUsers(realmName).get(userId);
if (userResource == null) {
log.error("❌ UserResource null pour l'utilisateur {} - Service Keycloak indisponible", userId);
throw new KeycloakServiceException.ServiceUnavailableException(
"Impossible de se connecter au service Keycloak pour envoyer l'email de vérification: " + userId);
}
userResource.sendVerifyEmail();
log.info("✅ Email de vérification envoyé: {}", userId);
} catch (KeycloakServiceException e) {
throw e;
} 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);
handleConnectionException(e, "envoi de l'email de vérification pour l'utilisateur " + userId);
}
}
@@ -347,14 +501,24 @@ public class UserServiceImpl implements UserService {
try {
UserResource userResource = keycloakAdminClient.getUsers(realmName).get(userId);
int sessionsCount = userResource.getUserSessions().size();
if (userResource == null) {
log.error("❌ UserResource null pour l'utilisateur {} - Service Keycloak indisponible", userId);
throw new KeycloakServiceException.ServiceUnavailableException(
"Impossible de se connecter au service Keycloak pour déconnecter les sessions: " + userId);
}
int sessionsCount = userResource.getUserSessions() != null ? userResource.getUserSessions().size() : 0;
userResource.logout();
log.info("✅ {} sessions révoquées pour l'utilisateur {}", sessionsCount, userId);
return sessionsCount;
} catch (KeycloakServiceException e) {
throw e;
} 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);
handleConnectionException(e, "déconnexion des sessions pour l'utilisateur " + userId);
return 0; // Ne sera jamais atteint car handleConnectionException lance une exception
}
}
@@ -365,8 +529,8 @@ public class UserServiceImpl implements UserService {
try {
UserResource userResource = keycloakAdminClient.getUsers(realmName).get(userId);
return userResource.getUserSessions().stream()
.map(session -> session.getId())
.collect(Collectors.toList());
.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();
@@ -386,10 +550,10 @@ public class UserServiceImpl implements UserService {
@Override
public UserSearchResultDTO getAllUsers(@NotBlank String realmName, int page, int pageSize) {
UserSearchCriteriaDTO criteria = UserSearchCriteriaDTO.builder()
.realmName(realmName)
.page(page)
.pageSize(pageSize)
.build();
.realmName(realmName)
.page(page)
.pageSize(pageSize)
.build();
return searchUsers(criteria);
}
@@ -398,7 +562,7 @@ public class UserServiceImpl implements UserService {
public boolean usernameExists(@NotBlank String username, @NotBlank String realmName) {
try {
List<UserRepresentation> users = keycloakAdminClient.getUsers(realmName)
.search(username, 0, 1, true);
.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);
@@ -410,7 +574,7 @@ public class UserServiceImpl implements UserService {
public boolean emailExists(@NotBlank String email, @NotBlank String realmName) {
try {
List<UserRepresentation> users = keycloakAdminClient.getUsers(realmName)
.searchByEmail(email, true);
.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);
@@ -420,19 +584,188 @@ public class UserServiceImpl implements UserService {
@Override
public String exportUsersToCSV(@NotNull UserSearchCriteriaDTO criteria) {
// TODO: Implémenter l'export CSV
throw new UnsupportedOperationException("Export CSV non implémenté");
log.info("Export des utilisateurs en CSV pour le realm {}", criteria.getRealmName());
// Disable pagination for export to get all users
int originalPageSize = criteria.getPageSize();
criteria.setPageSize(10000); // Set a large limit or handle pagination loops
UserSearchResultDTO result = searchUsers(criteria);
criteria.setPageSize(originalPageSize); // Restore
StringBuilder csv = new StringBuilder();
csv.append("username,email,firstName,lastName,enabled\n");
for (UserDTO user : result.getUsers()) {
csv.append(escape(user.getUsername())).append(",");
csv.append(escape(user.getEmail())).append(",");
csv.append(escape(user.getPrenom())).append(",");
csv.append(escape(user.getNom())).append(",");
csv.append(user.getEnabled() != null ? user.getEnabled() : true).append("\n");
}
return csv.toString();
}
@Override
public int importUsersFromCSV(@NotBlank String csvContent, @NotBlank String realmName) {
// TODO: Implémenter l'import CSV
throw new UnsupportedOperationException("Import CSV non implémenté");
log.info("Import des utilisateurs depuis CSV pour le realm {}", realmName);
String[] lines = csvContent.split("\\r?\\n");
int count = 0;
int startIndex = 0;
// Skip header if present
if (lines.length > 0 && lines[0].toLowerCase().startsWith("username")) {
startIndex = 1;
}
for (int i = startIndex; i < lines.length; i++) {
String line = lines[i].trim();
if (line.isEmpty())
continue;
try {
String[] parts = parseCSVLine(line);
if (parts.length < 5) {
log.warn("Ligne CSV invalide ignorée (pas assez de colonnes): {}", line);
continue;
}
String username = parts[0];
String email = parts[1];
String firstName = parts[2];
String lastName = parts[3];
boolean enabled = Boolean.parseBoolean(parts[4]);
if (username == null || username.isBlank()) {
log.warn("Username manquant à la ligne {}", i + 1);
continue;
}
UserDTO userDTO = UserDTO.builder()
.username(username)
.email(email.isBlank() ? null : email)
.prenom(firstName.isBlank() ? null : firstName)
.nom(lastName.isBlank() ? null : lastName)
.enabled(enabled)
.build();
createUser(userDTO, realmName);
count++;
} catch (Exception e) {
log.error("Erreur lors de l'import de la ligne {}: {}", i + 1, e.getMessage());
// Continue with next line
}
}
log.info("✅ {} utilisateurs importés avec succès", count);
return count;
}
private String escape(String data) {
if (data == null)
return "";
String escapedData = data.replaceAll("\"", "\"\"");
if (escapedData.contains(",") || escapedData.contains("\n") || escapedData.contains("\"")) {
return "\"" + escapedData + "\"";
}
return escapedData;
}
private String[] parseCSVLine(String line) {
// Simple regex to split by comma but ignoring commas inside quotes
// This regex handles: "value",value,"val,ue"
String[] tokens = line.split(",(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)", -1);
for (int i = 0; i < tokens.length; i++) {
String token = tokens[i].trim();
if (token.startsWith("\"") && token.endsWith("\"")) {
token = token.substring(1, token.length() - 1);
token = token.replaceAll("\"\"", "\"");
}
tokens[i] = token;
}
return tokens;
}
// ==================== Méthodes privées ====================
private void setPassword(String userId, String realmName, String password, boolean temporary) {
/**
* Valide une réponse HTTP du service Keycloak.
*
* @param response La réponse à valider
* @param operation Nom de l'opération (pour les logs)
* @param expectedStatus Le code de statut HTTP attendu
* @throws KeycloakServiceException si la réponse est null ou a un code d'erreur
*/
private void validateResponse(Response response, String operation, int expectedStatus) {
if (response == null) {
log.error("❌ Réponse null lors de l'opération {} - Service Keycloak indisponible", operation);
throw new KeycloakServiceException.ServiceUnavailableException(
"Impossible de se connecter au service Keycloak pour l'opération: " + operation);
}
int status = response.getStatus();
if (status != expectedStatus) {
String errorMessage = "Échec de l'opération: " + operation;
if (response.getStatusInfo() != null) {
errorMessage += " - " + response.getStatusInfo();
}
// Gérer les différents codes d'erreur HTTP
if (status == 400) {
throw new KeycloakServiceException("Données invalides: " + errorMessage, status);
} else if (status == 401) {
throw new KeycloakServiceException("Non autorisé: " + errorMessage, status);
} else if (status == 403) {
throw new KeycloakServiceException("Accès interdit: " + errorMessage, status);
} else if (status == 404) {
throw new KeycloakServiceException("Ressource non trouvée: " + errorMessage, status);
} else if (status == 409) {
throw new KeycloakServiceException("Conflit: " + errorMessage, status);
} else if (status == 500) {
throw new KeycloakServiceException("Erreur serveur interne Keycloak: " + errorMessage, status);
} else if (status == 502 || status == 503) {
throw new KeycloakServiceException.ServiceUnavailableException("Service Keycloak indisponible: " + errorMessage);
} else if (status == 504) {
throw new KeycloakServiceException.TimeoutException("Timeout lors de l'opération: " + operation);
} else {
throw new KeycloakServiceException(errorMessage, status);
}
}
}
/**
* Gère les exceptions de connexion et les convertit en KeycloakServiceException appropriée.
*
* @throws KeycloakServiceException toujours (lève une exception)
*/
private void handleConnectionException(Exception e, String operation) throws KeycloakServiceException {
String errorMessage = e.getMessage();
if (e instanceof ConnectException ||
e instanceof SocketTimeoutException ||
(errorMessage != null && (errorMessage.contains("Connection") ||
errorMessage.contains("timeout") ||
errorMessage.contains("refused") ||
errorMessage.contains("Unable to connect")))) {
log.error("❌ Erreur de connexion au service Keycloak lors de l'opération {}", operation, e);
throw new KeycloakServiceException.ServiceUnavailableException(
"Erreur de connexion au service Keycloak: " + (errorMessage != null ? errorMessage : e.getClass().getSimpleName()), e);
}
// Pour les autres exceptions, vérifier si c'est une KeycloakServiceException déjà
if (e instanceof KeycloakServiceException) {
throw (KeycloakServiceException) e;
}
// Sinon, encapsuler dans une KeycloakServiceException générique
throw new KeycloakServiceException("Erreur lors de l'opération " + operation + ": " +
(errorMessage != null ? errorMessage : e.getClass().getSimpleName()), e);
}
private void setPassword(String userId, String realmName, String password, boolean temporary) throws KeycloakServiceException {
try {
UserResource userResource = keycloakAdminClient.getUsers(realmName).get(userId);
@@ -446,27 +779,28 @@ public class UserServiceImpl implements UserService {
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);
handleConnectionException(e, "définition du mot de passe pour l'utilisateur " + userId);
}
}
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;
}
.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;
}
// Filtrer par emailVerified
if (criteria.getEmailVerified() != null
&& !criteria.getEmailVerified().equals(user.isEmailVerified())) {
return false;
}
// TODO: Ajouter d'autres filtres selon les besoins
// TODO: Ajouter d'autres filtres selon les besoins
return true;
})
.collect(Collectors.toList());
return true;
})
.collect(Collectors.toList());
}
}

View File

@@ -1,99 +1,105 @@
# ============================================================================
# Lions User Manager - Server Implementation Configuration - DEV
# Lions User Manager Server - Configuration Développement
# ============================================================================
# Ce fichier contient TOUTES les propriétés spécifiques au développement
# Il surcharge et complète application.properties
# ============================================================================
# HTTP Configuration
# ============================================
# HTTP Configuration DEV
# ============================================
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)
# Backend n'utilise PAS OIDC - il utilise directement l'Admin API
# CORS permissif en dev
quarkus.http.cors.origins=*
# ============================================
# Logging DEV (plus verbeux)
# ============================================
quarkus.log.level=DEBUG
quarkus.log.category."dev.lions.user.manager".level=TRACE
quarkus.log.category."org.keycloak".level=DEBUG
quarkus.log.category."io.quarkus.oidc".level=DEBUG
quarkus.log.category."io.quarkus.oidc.runtime".level=DEBUG
quarkus.log.category."io.quarkus.security".level=DEBUG
quarkus.log.console.format=%d{HH:mm:ss} %-5p [%c{2.}] (%t) %s%e%n
# File Logging pour Audit (DEV)
quarkus.log.file.path=logs/dev/lions-user-manager.log
quarkus.log.file.rotation.max-backup-index=3
# ============================================
# OIDC Configuration DEV - DÉSACTIVÉ PAR DÉFAUT
# ============================================
# En mode DEV, on désactive OIDC sur le backend pour simplifier le développement
# Le client JSF est sécurisé, mais le backend accepte toutes les requêtes
# ATTENTION: NE JAMAIS utiliser cette config en production !
quarkus.oidc.enabled=false
quarkus.oidc.dev-ui.enabled=false
quarkus.oidc.discovery-enabled=false
# Keycloak Admin Client Configuration (DEV)
# Alternative: Si vous voulez activer OIDC en dev (pour tester le flow complet),
# commentez la ligne "quarkus.oidc.enabled=false" ci-dessus et décommentez ci-dessous:
#
# quarkus.oidc.enabled=true
# quarkus.oidc.auth-server-url=http://localhost:8180/realms/lions-user-manager
# quarkus.oidc.tls.verification=none
# quarkus.oidc.token.issuer=http://localhost:8180/realms/lions-user-manager
# quarkus.oidc.discovery-enabled=true
# quarkus.oidc.token.audience=account
# quarkus.oidc.verify-access-token=true
# quarkus.oidc.roles.role-claim-path=realm_access/roles
# quarkus.security.auth.enabled=true
# ============================================
# Keycloak Admin Client Configuration DEV
# ============================================
# Configuration pour accéder à l'API Admin de Keycloak local
# IMPORTANT: L'utilisateur admin se trouve dans le realm "master", pas "lions-user-manager"
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=lions-user-manager,master,btpxpress,test-realm
# Timeout augmenté pour Keycloak local (peut être lent au démarrage)
lions.keycloak.timeout-seconds=60
# Circuit Breaker Configuration (DEV - plus permissif)
quarkus.smallrye-fault-tolerance.enabled=true
# Realms autorisés en dev
lions.keycloak.authorized-realms=lions-user-manager,btpxpress,master,unionflow
# 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
# ============================================
# Audit Configuration DEV
# ============================================
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=INFO
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.category."io.quarkus.oidc".level=WARN
quarkus.log.console.enable=true
quarkus.log.console.format=%d{HH:mm:ss} %-5p [%c{2.}] (%t) %s%e%n
# quarkus.log.console.color est déprécié dans Quarkus 3.x
# 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
# ============================================
# OpenAPI/Swagger Configuration DEV
# ============================================
quarkus.swagger-ui.enable=true
# Le chemin par défaut est /q/swagger-ui (pas besoin de le spécifier)
# Dev Services (activé en DEV)
quarkus.devservices.enabled=false
# Security Configuration (DEV)
# ============================================
# Security Configuration DEV
# ============================================
# Security désactivée en dev (car OIDC est désactivé)
quarkus.security.auth.enabled=false
quarkus.security.jaxrs.deny-unannotated-endpoints=false
# En dev, désactiver la vérification proactive de sécurité pour permettre @RolesAllowed
# de fonctionner sans authentification (pour faciliter les tests locaux)
# En prod, @RolesAllowed sera géré normalement par Quarkus Security avec OIDC/Keycloak
quarkus.security.auth.proactive=false
# Hot Reload
# Permissions HTTP - Accès public à tous les endpoints en DEV
quarkus.http.auth.permission.public.paths=/api/*,/q/*,/health/*,/metrics,/swagger-ui/*,/openapi
quarkus.http.auth.permission.public.policy=permit
# ============================================
# Hot Reload et Dev Mode
# ============================================
quarkus.live-reload.instrumentation=true
# Désactiver le continuous testing qui bloque le démarrage
quarkus.test.continuous-testing=disabled
quarkus.profile=dev
# Indexer les dépendances Keycloak pour éviter les warnings
# ============================================
# Indexation des dépendances Keycloak
# ============================================
quarkus.index-dependency.keycloak-admin.group-id=org.keycloak
quarkus.index-dependency.keycloak-admin.artifact-id=keycloak-admin-client
quarkus.index-dependency.keycloak-core.group-id=org.keycloak
quarkus.index-dependency.keycloak-core.artifact-id=keycloak-core
# Jackson - Ignorer les propriétés inconnues pour compatibilité Keycloak
quarkus.jackson.fail-on-unknown-properties=false

View File

@@ -1,113 +1,119 @@
# ============================================================================
# Lions User Manager - Server Implementation Configuration - PRODUCTION
# Lions User Manager Server - Configuration Production
# ============================================================================
# Ce fichier contient TOUTES les propriétés spécifiques à la production
# Il surcharge et complète application.properties
# ============================================================================
# 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=*
# ============================================
# HTTP Configuration PROD
# ============================================
quarkus.http.port=8080
# Keycloak OIDC Configuration (PROD)
quarkus.oidc.auth-server-url=https://security.lions.dev/realms/master
quarkus.oidc.client-id=lions-user-manager
# CORS restrictif en production (via variable d'environnement)
quarkus.http.cors.origins=${CORS_ORIGINS:https://btpxpress.lions.dev,https://admin.lions.dev}
# ============================================
# Logging PROD (moins verbeux)
# ============================================
quarkus.log.level=INFO
quarkus.log.category."dev.lions.user.manager".level=INFO
quarkus.log.category."org.keycloak".level=WARN
quarkus.log.console.format=%d{yyyy-MM-dd HH:mm:ss,SSS} %-5p [%c{3.}] (%t) %s%e%n
# File Logging pour Audit (PROD)
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
# ============================================
# OIDC Configuration PROD - OBLIGATOIRE ET ACTIF
# ============================================
quarkus.oidc.enabled=true
quarkus.oidc.auth-server-url=${KEYCLOAK_AUTH_SERVER_URL:https://security.lions.dev/realms/master}
quarkus.oidc.credentials.secret=${KEYCLOAK_CLIENT_SECRET}
quarkus.oidc.tls.verification=required
quarkus.oidc.application-type=service
quarkus.oidc.token.issuer=${KEYCLOAK_AUTH_SERVER_URL:https://security.lions.dev/realms/master}
# Keycloak Admin Client Configuration (PROD)
lions.keycloak.server-url=https://security.lions.dev
lions.keycloak.admin-realm=master
# Vérification TLS requise en production
quarkus.oidc.tls.verification=required
# Vérification stricte des tokens
quarkus.oidc.discovery-enabled=true
quarkus.oidc.verify-access-token=true
# Extraction des rôles
quarkus.oidc.roles.role-claim-path=realm_access/roles
# ============================================
# Keycloak Admin Client Configuration PROD
# ============================================
lions.keycloak.server-url=${KEYCLOAK_SERVER_URL:https://security.lions.dev}
lions.keycloak.admin-realm=${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}
# Pool de connexions augmenté en production
lions.keycloak.connection-pool-size=20
lions.keycloak.timeout-seconds=60
# Realms autorisés (PROD)
lions.keycloak.authorized-realms=btpxpress,lions-realm
# Realms autorisés en production (via variable d'environnement)
lions.keycloak.authorized-realms=${KEYCLOAK_AUTHORIZED_REALMS:btpxpress,master,unionflow}
# Circuit Breaker Configuration (PROD - strict)
quarkus.smallrye-fault-tolerance.enabled=true
# Retry Configuration (PROD)
# ============================================
# 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
# ============================================
# Audit Configuration PROD
# ============================================
lions.audit.retention-days=365
lions.audit.log-to-database=true
# Database Configuration (PROD - obligatoire pour audit)
# ============================================
# Database Configuration PROD (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.enabled=true
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)
# ============================================
# OpenAPI/Swagger Configuration PROD
# ============================================
# Swagger désactivé en production 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)
# ============================================
# Security Configuration PROD (strict)
# ============================================
quarkus.security.auth.enabled=true
quarkus.security.jaxrs.deny-unannotated-endpoints=true
quarkus.security.auth.proactive=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
# ============================================
# Performance tuning PROD
# ============================================
quarkus.thread-pool.core-threads=4
quarkus.thread-pool.max-threads=32
quarkus.thread-pool.queue-size=200
# 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}
# ============================================
# SSL/TLS Configuration PROD (optionnel)
# ============================================
# Décommenter si le serveur gère le SSL directement (sinon géré par Ingress/Load Balancer)
# 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

View File

@@ -1,61 +1,58 @@
# ============================================================================
# Lions User Manager - Server Implementation Configuration
# Lions User Manager Server - Configuration Commune
# ============================================================================
# Ce fichier contient UNIQUEMENT la configuration commune à tous les environnements
# Les configurations spécifiques sont dans:
# - application-dev.properties (développement)
# - application-prod.properties (production)
# ============================================================================
# ============================================
# Application Info
# ============================================
quarkus.application.name=lions-user-manager-server
quarkus.application.version=1.0.0
# HTTP Configuration
quarkus.http.port=8081
# ============================================
# HTTP Configuration (commune)
# ============================================
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
# ============================================
# Keycloak OIDC Configuration (base commune)
# ============================================
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}
# ============================================
# Keycloak Admin Client Configuration (base commune)
# ============================================
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
# ============================================
# Database Configuration (désactivé par défaut)
# ============================================
# Désactiver Hibernate ORM si aucune entité JPA n'est utilisée
quarkus.hibernate-orm.enabled=false
# Logging Configuration
# ============================================
# Logging Configuration (base commune)
# ============================================
quarkus.log.level=INFO
quarkus.log.category."dev.lions.user.manager".level=DEBUG
quarkus.log.category."org.keycloak".level=WARN
@@ -69,32 +66,43 @@ 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
# Le chemin par défaut est /q/swagger-ui (pas besoin de le spécifier)
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)
# ============================================
# Dev Services (désactivé par défaut)
# ============================================
quarkus.devservices.enabled=false