diff --git a/lions-user-manager-server-impl-quarkus/src/main/java/dev/lions/user/manager/mapper/RoleMapper.java b/lions-user-manager-server-impl-quarkus/src/main/java/dev/lions/user/manager/mapper/RoleMapper.java new file mode 100644 index 0000000..f70e04b --- /dev/null +++ b/lions-user-manager-server-impl-quarkus/src/main/java/dev/lions/user/manager/mapper/RoleMapper.java @@ -0,0 +1,76 @@ +package dev.lions.user.manager.mapper; + +import dev.lions.user.manager.dto.role.RoleDTO; +import dev.lions.user.manager.enums.role.TypeRole; +import org.keycloak.representations.idm.RoleRepresentation; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * Mapper pour convertir entre RoleDTO et Keycloak RoleRepresentation + */ +public class RoleMapper { + + /** + * Convertit une RoleRepresentation Keycloak en RoleDTO + */ + public static RoleDTO toDTO(RoleRepresentation roleRep, String realmName, TypeRole typeRole) { + if (roleRep == null) { + return null; + } + + return RoleDTO.builder() + .id(roleRep.getId()) + .nom(roleRep.getName()) + .description(roleRep.getDescription()) + .typeRole(typeRole) + .realmName(realmName) + .composite(roleRep.isComposite() != null ? roleRep.isComposite() : false) + .build(); + } + + /** + * Convertit un RoleDTO en RoleRepresentation Keycloak + */ + public static RoleRepresentation toRepresentation(RoleDTO roleDTO) { + if (roleDTO == null) { + return null; + } + + RoleRepresentation roleRep = new RoleRepresentation(); + roleRep.setId(roleDTO.getId()); + roleRep.setName(roleDTO.getNom()); + roleRep.setDescription(roleDTO.getDescription()); + roleRep.setComposite(roleDTO.isComposite()); + roleRep.setClientRole(roleDTO.getTypeRole() == TypeRole.CLIENT_ROLE); + + return roleRep; + } + + /** + * Convertit une liste de RoleRepresentation en liste de RoleDTO + */ + public static List toDTOList(List roleReps, String realmName, TypeRole typeRole) { + if (roleReps == null) { + return List.of(); + } + + return roleReps.stream() + .map(roleRep -> toDTO(roleRep, realmName, typeRole)) + .collect(Collectors.toList()); + } + + /** + * Convertit une liste de RoleDTO en liste de RoleRepresentation + */ + public static List toRepresentationList(List roleDTOs) { + if (roleDTOs == null) { + return List.of(); + } + + return roleDTOs.stream() + .map(RoleMapper::toRepresentation) + .collect(Collectors.toList()); + } +} diff --git a/lions-user-manager-server-impl-quarkus/src/main/java/dev/lions/user/manager/resource/RoleResource.java b/lions-user-manager-server-impl-quarkus/src/main/java/dev/lions/user/manager/resource/RoleResource.java new file mode 100644 index 0000000..826ec1d --- /dev/null +++ b/lions-user-manager-server-impl-quarkus/src/main/java/dev/lions/user/manager/resource/RoleResource.java @@ -0,0 +1,509 @@ +package dev.lions.user.manager.resource; + +import dev.lions.user.manager.dto.role.RoleDTO; +import dev.lions.user.manager.service.RoleService; +import jakarta.annotation.security.RolesAllowed; +import jakarta.inject.Inject; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.media.Content; +import org.eclipse.microprofile.openapi.annotations.media.Schema; +import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponses; +import org.eclipse.microprofile.openapi.annotations.tags.Tag; + +import java.util.List; + +/** + * REST Resource pour la gestion des rôles Keycloak + * Endpoints pour les rôles realm, rôles client, et attributions + */ +@Path("/api/roles") +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +@Tag(name = "Roles", description = "Gestion des rôles Keycloak (realm et client)") +@Slf4j +public class RoleResource { + + @Inject + RoleService roleService; + + // ==================== Endpoints Realm Roles ==================== + + @POST + @Path("/realm") + @Operation(summary = "Créer un rôle realm", description = "Crée un nouveau rôle au niveau du realm") + @APIResponses({ + @APIResponse(responseCode = "201", description = "Rôle créé", + content = @Content(schema = @Schema(implementation = RoleDTO.class))), + @APIResponse(responseCode = "400", description = "Données invalides"), + @APIResponse(responseCode = "409", description = "Rôle existe déjà"), + @APIResponse(responseCode = "500", description = "Erreur serveur") + }) + @RolesAllowed({"admin", "role_manager"}) + public Response createRealmRole( + @Valid @NotNull RoleDTO roleDTO, + @QueryParam("realm") @NotBlank String realmName + ) { + log.info("POST /api/roles/realm - Création du rôle realm: {} dans le realm: {}", + roleDTO.getNom(), realmName); + + try { + RoleDTO createdRole = roleService.createRealmRole(roleDTO, realmName); + return Response.status(Response.Status.CREATED).entity(createdRole).build(); + } catch (IllegalArgumentException e) { + log.warn("Données invalides lors de la création du rôle: {}", e.getMessage()); + return Response.status(Response.Status.CONFLICT) + .entity(new ErrorResponse(e.getMessage())) + .build(); + } catch (Exception e) { + log.error("Erreur lors de la création du rôle realm", e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(new ErrorResponse(e.getMessage())) + .build(); + } + } + + @GET + @Path("/realm/{roleName}") + @Operation(summary = "Récupérer un rôle realm par nom", description = "Récupère les détails d'un rôle realm") + @APIResponses({ + @APIResponse(responseCode = "200", description = "Rôle trouvé", + content = @Content(schema = @Schema(implementation = RoleDTO.class))), + @APIResponse(responseCode = "404", description = "Rôle non trouvé"), + @APIResponse(responseCode = "500", description = "Erreur serveur") + }) + @RolesAllowed({"admin", "role_manager", "role_viewer"}) + public Response getRealmRole( + @Parameter(description = "Nom du rôle") @PathParam("roleName") @NotBlank String roleName, + @Parameter(description = "Nom du realm") @QueryParam("realm") @NotBlank String realmName + ) { + log.info("GET /api/roles/realm/{} - realm: {}", roleName, realmName); + + try { + return roleService.getRealmRoleByName(roleName, realmName) + .map(role -> Response.ok(role).build()) + .orElse(Response.status(Response.Status.NOT_FOUND) + .entity(new ErrorResponse("Rôle non trouvé")) + .build()); + } catch (Exception e) { + log.error("Erreur lors de la récupération du rôle realm {}", roleName, e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(new ErrorResponse(e.getMessage())) + .build(); + } + } + + @GET + @Path("/realm") + @Operation(summary = "Lister tous les rôles realm", description = "Liste tous les rôles du realm") + @APIResponses({ + @APIResponse(responseCode = "200", description = "Liste des rôles"), + @APIResponse(responseCode = "500", description = "Erreur serveur") + }) + @RolesAllowed({"admin", "role_manager", "role_viewer"}) + public Response getAllRealmRoles( + @QueryParam("realm") @NotBlank String realmName + ) { + log.info("GET /api/roles/realm - realm: {}", realmName); + + try { + List roles = roleService.getAllRealmRoles(realmName); + return Response.ok(roles).build(); + } catch (Exception e) { + log.error("Erreur lors de la récupération des rôles realm", e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(new ErrorResponse(e.getMessage())) + .build(); + } + } + + @PUT + @Path("/realm/{roleName}") + @Operation(summary = "Mettre à jour un rôle realm", description = "Met à jour les informations d'un rôle realm") + @APIResponses({ + @APIResponse(responseCode = "200", description = "Rôle mis à jour", + content = @Content(schema = @Schema(implementation = RoleDTO.class))), + @APIResponse(responseCode = "404", description = "Rôle non trouvé"), + @APIResponse(responseCode = "500", description = "Erreur serveur") + }) + @RolesAllowed({"admin", "role_manager"}) + public Response updateRealmRole( + @PathParam("roleName") @NotBlank String roleName, + @Valid @NotNull RoleDTO roleDTO, + @QueryParam("realm") @NotBlank String realmName + ) { + log.info("PUT /api/roles/realm/{} - realm: {}", roleName, realmName); + + try { + RoleDTO updatedRole = roleService.updateRealmRole(roleName, roleDTO, realmName); + return Response.ok(updatedRole).build(); + } catch (Exception e) { + log.error("Erreur lors de la mise à jour du rôle realm {}", roleName, e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(new ErrorResponse(e.getMessage())) + .build(); + } + } + + @DELETE + @Path("/realm/{roleName}") + @Operation(summary = "Supprimer un rôle realm", description = "Supprime un rôle realm") + @APIResponses({ + @APIResponse(responseCode = "204", description = "Rôle supprimé"), + @APIResponse(responseCode = "404", description = "Rôle non trouvé"), + @APIResponse(responseCode = "500", description = "Erreur serveur") + }) + @RolesAllowed({"admin"}) + public Response deleteRealmRole( + @PathParam("roleName") @NotBlank String roleName, + @QueryParam("realm") @NotBlank String realmName + ) { + log.info("DELETE /api/roles/realm/{} - realm: {}", roleName, realmName); + + try { + roleService.deleteRealmRole(roleName, realmName); + return Response.noContent().build(); + } catch (Exception e) { + log.error("Erreur lors de la suppression du rôle realm {}", roleName, e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(new ErrorResponse(e.getMessage())) + .build(); + } + } + + // ==================== Endpoints Client Roles ==================== + + @POST + @Path("/client/{clientId}") + @Operation(summary = "Créer un rôle client", description = "Crée un nouveau rôle pour un client spécifique") + @APIResponses({ + @APIResponse(responseCode = "201", description = "Rôle créé", + content = @Content(schema = @Schema(implementation = RoleDTO.class))), + @APIResponse(responseCode = "400", description = "Données invalides"), + @APIResponse(responseCode = "409", description = "Rôle existe déjà"), + @APIResponse(responseCode = "500", description = "Erreur serveur") + }) + @RolesAllowed({"admin", "role_manager"}) + public Response createClientRole( + @PathParam("clientId") @NotBlank String clientId, + @Valid @NotNull RoleDTO roleDTO, + @QueryParam("realm") @NotBlank String realmName + ) { + log.info("POST /api/roles/client/{} - Création du rôle client dans le realm: {}", + clientId, realmName); + + try { + RoleDTO createdRole = roleService.createClientRole(roleDTO, clientId, realmName); + return Response.status(Response.Status.CREATED).entity(createdRole).build(); + } catch (IllegalArgumentException e) { + log.warn("Données invalides lors de la création du rôle client: {}", e.getMessage()); + return Response.status(Response.Status.CONFLICT) + .entity(new ErrorResponse(e.getMessage())) + .build(); + } catch (Exception e) { + log.error("Erreur lors de la création du rôle client", e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(new ErrorResponse(e.getMessage())) + .build(); + } + } + + @GET + @Path("/client/{clientId}/{roleName}") + @Operation(summary = "Récupérer un rôle client par nom", description = "Récupère les détails d'un rôle client") + @APIResponses({ + @APIResponse(responseCode = "200", description = "Rôle trouvé", + content = @Content(schema = @Schema(implementation = RoleDTO.class))), + @APIResponse(responseCode = "404", description = "Rôle non trouvé"), + @APIResponse(responseCode = "500", description = "Erreur serveur") + }) + @RolesAllowed({"admin", "role_manager", "role_viewer"}) + public Response getClientRole( + @PathParam("clientId") @NotBlank String clientId, + @PathParam("roleName") @NotBlank String roleName, + @QueryParam("realm") @NotBlank String realmName + ) { + log.info("GET /api/roles/client/{}/{} - realm: {}", clientId, roleName, realmName); + + try { + return roleService.getClientRoleByName(roleName, clientId, realmName) + .map(role -> Response.ok(role).build()) + .orElse(Response.status(Response.Status.NOT_FOUND) + .entity(new ErrorResponse("Rôle client non trouvé")) + .build()); + } catch (Exception e) { + log.error("Erreur lors de la récupération du rôle client {}/{}", clientId, roleName, e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(new ErrorResponse(e.getMessage())) + .build(); + } + } + + @GET + @Path("/client/{clientId}") + @Operation(summary = "Lister tous les rôles d'un client", description = "Liste tous les rôles d'un client spécifique") + @APIResponses({ + @APIResponse(responseCode = "200", description = "Liste des rôles"), + @APIResponse(responseCode = "500", description = "Erreur serveur") + }) + @RolesAllowed({"admin", "role_manager", "role_viewer"}) + public Response getAllClientRoles( + @PathParam("clientId") @NotBlank String clientId, + @QueryParam("realm") @NotBlank String realmName + ) { + log.info("GET /api/roles/client/{} - realm: {}", clientId, realmName); + + try { + List roles = roleService.getAllClientRoles(clientId, realmName); + return Response.ok(roles).build(); + } catch (Exception e) { + log.error("Erreur lors de la récupération des rôles du client {}", clientId, e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(new ErrorResponse(e.getMessage())) + .build(); + } + } + + @DELETE + @Path("/client/{clientId}/{roleName}") + @Operation(summary = "Supprimer un rôle client", description = "Supprime un rôle d'un client") + @APIResponses({ + @APIResponse(responseCode = "204", description = "Rôle supprimé"), + @APIResponse(responseCode = "404", description = "Rôle non trouvé"), + @APIResponse(responseCode = "500", description = "Erreur serveur") + }) + @RolesAllowed({"admin"}) + public Response deleteClientRole( + @PathParam("clientId") @NotBlank String clientId, + @PathParam("roleName") @NotBlank String roleName, + @QueryParam("realm") @NotBlank String realmName + ) { + log.info("DELETE /api/roles/client/{}/{} - realm: {}", clientId, roleName, realmName); + + try { + roleService.deleteClientRole(roleName, clientId, realmName); + return Response.noContent().build(); + } catch (Exception e) { + log.error("Erreur lors de la suppression du rôle client {}/{}", clientId, roleName, e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(new ErrorResponse(e.getMessage())) + .build(); + } + } + + // ==================== Endpoints Attribution de rôles ==================== + + @POST + @Path("/assign/realm/{userId}") + @Operation(summary = "Attribuer des rôles realm à un utilisateur", description = "Assigne un ou plusieurs rôles realm à un utilisateur") + @APIResponses({ + @APIResponse(responseCode = "204", description = "Rôles attribués"), + @APIResponse(responseCode = "404", description = "Utilisateur ou rôle non trouvé"), + @APIResponse(responseCode = "500", description = "Erreur serveur") + }) + @RolesAllowed({"admin", "role_manager"}) + public Response assignRealmRoles( + @PathParam("userId") @NotBlank String userId, + @QueryParam("realm") @NotBlank String realmName, + @NotNull RoleAssignmentRequest request + ) { + log.info("POST /api/roles/assign/realm/{} - Attribution de {} rôles", userId, request.roleNames.size()); + + try { + roleService.assignRealmRolesToUser(userId, request.roleNames, realmName); + return Response.noContent().build(); + } catch (Exception e) { + log.error("Erreur lors de l'attribution des rôles realm à l'utilisateur {}", userId, e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(new ErrorResponse(e.getMessage())) + .build(); + } + } + + @POST + @Path("/revoke/realm/{userId}") + @Operation(summary = "Révoquer des rôles realm d'un utilisateur", description = "Révoque un ou plusieurs rôles realm d'un utilisateur") + @APIResponses({ + @APIResponse(responseCode = "204", description = "Rôles révoqués"), + @APIResponse(responseCode = "404", description = "Utilisateur ou rôle non trouvé"), + @APIResponse(responseCode = "500", description = "Erreur serveur") + }) + @RolesAllowed({"admin", "role_manager"}) + public Response revokeRealmRoles( + @PathParam("userId") @NotBlank String userId, + @QueryParam("realm") @NotBlank String realmName, + @NotNull RoleAssignmentRequest request + ) { + log.info("POST /api/roles/revoke/realm/{} - Révocation de {} rôles", userId, request.roleNames.size()); + + try { + roleService.revokeRealmRolesFromUser(userId, request.roleNames, realmName); + return Response.noContent().build(); + } catch (Exception e) { + log.error("Erreur lors de la révocation des rôles realm de l'utilisateur {}", userId, e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(new ErrorResponse(e.getMessage())) + .build(); + } + } + + @POST + @Path("/assign/client/{clientId}/{userId}") + @Operation(summary = "Attribuer des rôles client à un utilisateur", description = "Assigne un ou plusieurs rôles client à un utilisateur") + @APIResponses({ + @APIResponse(responseCode = "204", description = "Rôles attribués"), + @APIResponse(responseCode = "404", description = "Utilisateur, client ou rôle non trouvé"), + @APIResponse(responseCode = "500", description = "Erreur serveur") + }) + @RolesAllowed({"admin", "role_manager"}) + public Response assignClientRoles( + @PathParam("clientId") @NotBlank String clientId, + @PathParam("userId") @NotBlank String userId, + @QueryParam("realm") @NotBlank String realmName, + @NotNull RoleAssignmentRequest request + ) { + log.info("POST /api/roles/assign/client/{}/{} - Attribution de {} rôles client", + clientId, userId, request.roleNames.size()); + + try { + roleService.assignClientRolesToUser(userId, clientId, request.roleNames, realmName); + return Response.noContent().build(); + } catch (Exception e) { + log.error("Erreur lors de l'attribution des rôles client à l'utilisateur {}", userId, e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(new ErrorResponse(e.getMessage())) + .build(); + } + } + + @GET + @Path("/user/realm/{userId}") + @Operation(summary = "Récupérer les rôles realm d'un utilisateur", description = "Liste tous les rôles realm d'un utilisateur") + @APIResponses({ + @APIResponse(responseCode = "200", description = "Liste des rôles"), + @APIResponse(responseCode = "500", description = "Erreur serveur") + }) + @RolesAllowed({"admin", "role_manager", "role_viewer"}) + public Response getUserRealmRoles( + @PathParam("userId") @NotBlank String userId, + @QueryParam("realm") @NotBlank String realmName + ) { + log.info("GET /api/roles/user/realm/{} - realm: {}", userId, realmName); + + try { + List roles = roleService.getUserRealmRoles(userId, realmName); + return Response.ok(roles).build(); + } catch (Exception e) { + log.error("Erreur lors de la récupération des rôles realm de l'utilisateur {}", userId, e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(new ErrorResponse(e.getMessage())) + .build(); + } + } + + @GET + @Path("/user/client/{clientId}/{userId}") + @Operation(summary = "Récupérer les rôles client d'un utilisateur", description = "Liste tous les rôles client d'un utilisateur") + @APIResponses({ + @APIResponse(responseCode = "200", description = "Liste des rôles"), + @APIResponse(responseCode = "500", description = "Erreur serveur") + }) + @RolesAllowed({"admin", "role_manager", "role_viewer"}) + public Response getUserClientRoles( + @PathParam("clientId") @NotBlank String clientId, + @PathParam("userId") @NotBlank String userId, + @QueryParam("realm") @NotBlank String realmName + ) { + log.info("GET /api/roles/user/client/{}/{} - realm: {}", clientId, userId, realmName); + + try { + List roles = roleService.getUserClientRoles(userId, clientId, realmName); + return Response.ok(roles).build(); + } catch (Exception e) { + log.error("Erreur lors de la récupération des rôles client de l'utilisateur {}", userId, e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(new ErrorResponse(e.getMessage())) + .build(); + } + } + + // ==================== Endpoints Rôles composites ==================== + + @POST + @Path("/composite/{roleName}/add") + @Operation(summary = "Ajouter des rôles composites", description = "Ajoute des rôles composites à un rôle") + @APIResponses({ + @APIResponse(responseCode = "204", description = "Composites ajoutés"), + @APIResponse(responseCode = "404", description = "Rôle non trouvé"), + @APIResponse(responseCode = "500", description = "Erreur serveur") + }) + @RolesAllowed({"admin", "role_manager"}) + public Response addComposites( + @PathParam("roleName") @NotBlank String roleName, + @QueryParam("realm") @NotBlank String realmName, + @NotNull RoleAssignmentRequest request + ) { + log.info("POST /api/roles/composite/{}/add - Ajout de {} composites", roleName, request.roleNames.size()); + + try { + roleService.addCompositesToRealmRole(roleName, request.roleNames, realmName); + return Response.noContent().build(); + } catch (Exception e) { + log.error("Erreur lors de l'ajout des composites au rôle {}", roleName, e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(new ErrorResponse(e.getMessage())) + .build(); + } + } + + @GET + @Path("/composite/{roleName}") + @Operation(summary = "Récupérer les rôles composites", description = "Liste tous les rôles composites d'un rôle") + @APIResponses({ + @APIResponse(responseCode = "200", description = "Liste des composites"), + @APIResponse(responseCode = "500", description = "Erreur serveur") + }) + @RolesAllowed({"admin", "role_manager", "role_viewer"}) + public Response getComposites( + @PathParam("roleName") @NotBlank String roleName, + @QueryParam("realm") @NotBlank String realmName + ) { + log.info("GET /api/roles/composite/{} - realm: {}", roleName, realmName); + + try { + List composites = roleService.getCompositeRoles(roleName, realmName); + return Response.ok(composites).build(); + } catch (Exception e) { + log.error("Erreur lors de la récupération des composites du rôle {}", roleName, e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(new ErrorResponse(e.getMessage())) + .build(); + } + } + + // ==================== DTOs internes ==================== + + @Schema(description = "Requête d'attribution/révocation de rôles") + public static class RoleAssignmentRequest { + @Schema(description = "Liste des noms de rôles", required = true) + public List roleNames; + } + + @Schema(description = "Réponse d'erreur") + public static class ErrorResponse { + @Schema(description = "Message d'erreur") + public String message; + + public ErrorResponse(String message) { + this.message = message; + } + } +} diff --git a/lions-user-manager-server-impl-quarkus/src/main/java/dev/lions/user/manager/service/impl/AuditServiceImpl.java b/lions-user-manager-server-impl-quarkus/src/main/java/dev/lions/user/manager/service/impl/AuditServiceImpl.java new file mode 100644 index 0000000..06ebf1f --- /dev/null +++ b/lions-user-manager-server-impl-quarkus/src/main/java/dev/lions/user/manager/service/impl/AuditServiceImpl.java @@ -0,0 +1,371 @@ +package dev.lions.user.manager.service.impl; + +import dev.lions.user.manager.dto.audit.AuditLogDTO; +import dev.lions.user.manager.enums.audit.TypeActionAudit; +import dev.lions.user.manager.service.AuditService; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.microprofile.config.inject.ConfigProperty; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +/** + * Implémentation du service d'audit + * + * NOTES: + * - Cette implémentation utilise un stockage en mémoire pour le développement + * - En production, il faudrait utiliser une base de données (PostgreSQL avec Panache) + * - Les logs sont également écrits via SLF4J pour être capturés par les systèmes de logging centralisés + */ +@ApplicationScoped +@Slf4j +public class AuditServiceImpl implements AuditService { + + // Stockage en mémoire (à remplacer par une DB en production) + private final Map auditLogs = new ConcurrentHashMap<>(); + + @ConfigProperty(name = "lions.audit.enabled", defaultValue = "true") + boolean auditEnabled; + + @ConfigProperty(name = "lions.audit.log-to-database", defaultValue = "false") + boolean logToDatabase; + + @Override + public AuditLogDTO logAction(@Valid @NotNull AuditLogDTO auditLog) { + if (!auditEnabled) { + log.debug("Audit désactivé, log ignoré"); + return auditLog; + } + + // Générer un ID si nécessaire + if (auditLog.getId() == null) { + auditLog.setId(UUID.randomUUID().toString()); + } + + // Ajouter le timestamp si nécessaire + if (auditLog.getDateAction() == null) { + auditLog.setDateAction(LocalDateTime.now()); + } + + // Log structuré pour les systèmes de logging (Graylog, Elasticsearch, etc.) + log.info("AUDIT | Type: {} | Acteur: {} | Ressource: {} | Succès: {} | IP: {} | Détails: {}", + auditLog.getTypeAction(), + auditLog.getActeurUsername(), + auditLog.getRessourceType() + ":" + auditLog.getRessourceId(), + auditLog.isSucces(), + auditLog.getAdresseIp(), + auditLog.getDetails()); + + // Stocker en mémoire + auditLogs.put(auditLog.getId(), auditLog); + + // TODO: Si logToDatabase = true, persister dans PostgreSQL via Panache + // Exemple: + // if (logToDatabase) { + // AuditLogEntity entity = AuditLogMapper.toEntity(auditLog); + // entity.persist(); + // } + + return auditLog; + } + + @Override + public void logSuccess(@NotBlank String acteurUsername, @NotNull TypeActionAudit typeAction, + @NotBlank String ressourceType, @NotBlank String ressourceId, + String adresseIp, String details) { + AuditLogDTO auditLog = AuditLogDTO.builder() + .acteurUsername(acteurUsername) + .typeAction(typeAction) + .ressourceType(ressourceType) + .ressourceId(ressourceId) + .succes(true) + .adresseIp(adresseIp) + .details(details) + .dateAction(LocalDateTime.now()) + .build(); + + logAction(auditLog); + } + + @Override + public void logFailure(@NotBlank String acteurUsername, @NotNull TypeActionAudit typeAction, + @NotBlank String ressourceType, @NotBlank String ressourceId, + String adresseIp, @NotBlank String messageErreur) { + AuditLogDTO auditLog = AuditLogDTO.builder() + .acteurUsername(acteurUsername) + .typeAction(typeAction) + .ressourceType(ressourceType) + .ressourceId(ressourceId) + .succes(false) + .adresseIp(adresseIp) + .messageErreur(messageErreur) + .dateAction(LocalDateTime.now()) + .build(); + + logAction(auditLog); + } + + @Override + public List searchLogs(@NotBlank String acteurUsername, LocalDateTime dateDebut, + LocalDateTime dateFin, TypeActionAudit typeAction, + String ressourceType, Boolean succes, + int page, int pageSize) { + log.debug("Recherche de logs d'audit: acteur={}, dateDebut={}, dateFin={}, typeAction={}, succes={}", + acteurUsername, dateDebut, dateFin, typeAction, succes); + + return auditLogs.values().stream() + .filter(log -> { + // Filtre par acteur (si spécifié et non "*") + if (acteurUsername != null && !"*".equals(acteurUsername) && + !acteurUsername.equals(log.getActeurUsername())) { + return false; + } + + // Filtre par date début + if (dateDebut != null && log.getDateAction().isBefore(dateDebut)) { + return false; + } + + // Filtre par date fin + if (dateFin != null && log.getDateAction().isAfter(dateFin)) { + return false; + } + + // Filtre par type d'action + if (typeAction != null && !typeAction.equals(log.getTypeAction())) { + return false; + } + + // Filtre par type de ressource + if (ressourceType != null && !ressourceType.equals(log.getRessourceType())) { + return false; + } + + // Filtre par succès/échec + if (succes != null && succes != log.isSucces()) { + return false; + } + + return true; + }) + .sorted((a, b) -> b.getDateAction().compareTo(a.getDateAction())) // Tri décroissant par date + .skip((long) page * pageSize) + .limit(pageSize) + .collect(Collectors.toList()); + } + + @Override + public List getLogsByActeur(@NotBlank String acteurUsername, int limit) { + log.debug("Récupération des {} derniers logs de l'acteur: {}", limit, acteurUsername); + + return auditLogs.values().stream() + .filter(log -> acteurUsername.equals(log.getActeurUsername())) + .sorted((a, b) -> b.getDateAction().compareTo(a.getDateAction())) + .limit(limit) + .collect(Collectors.toList()); + } + + @Override + public List getLogsByRessource(@NotBlank String ressourceType, + @NotBlank String ressourceId, int limit) { + log.debug("Récupération des {} derniers logs de la ressource: {}:{}", + limit, ressourceType, ressourceId); + + return auditLogs.values().stream() + .filter(log -> ressourceType.equals(log.getRessourceType()) && + ressourceId.equals(log.getRessourceId())) + .sorted((a, b) -> b.getDateAction().compareTo(a.getDateAction())) + .limit(limit) + .collect(Collectors.toList()); + } + + @Override + public List getLogsByAction(@NotNull TypeActionAudit typeAction, + LocalDateTime dateDebut, LocalDateTime dateFin, + int limit) { + log.debug("Récupération des {} logs de type: {} entre {} et {}", + limit, typeAction, dateDebut, dateFin); + + return auditLogs.values().stream() + .filter(log -> { + if (!typeAction.equals(log.getTypeAction())) { + return false; + } + if (dateDebut != null && log.getDateAction().isBefore(dateDebut)) { + return false; + } + if (dateFin != null && log.getDateAction().isAfter(dateFin)) { + return false; + } + return true; + }) + .sorted((a, b) -> b.getDateAction().compareTo(a.getDateAction())) + .limit(limit) + .collect(Collectors.toList()); + } + + @Override + public Map getActionStatistics(LocalDateTime dateDebut, LocalDateTime dateFin) { + log.debug("Calcul des statistiques d'actions entre {} et {}", dateDebut, dateFin); + + return auditLogs.values().stream() + .filter(log -> { + if (dateDebut != null && log.getDateAction().isBefore(dateDebut)) { + return false; + } + if (dateFin != null && log.getDateAction().isAfter(dateFin)) { + return false; + } + return true; + }) + .collect(Collectors.groupingBy( + AuditLogDTO::getTypeAction, + Collectors.counting() + )); + } + + @Override + public Map getUserActivityStatistics(LocalDateTime dateDebut, LocalDateTime dateFin) { + log.debug("Calcul des statistiques d'activité utilisateurs entre {} et {}", dateDebut, dateFin); + + return auditLogs.values().stream() + .filter(log -> { + if (dateDebut != null && log.getDateAction().isBefore(dateDebut)) { + return false; + } + if (dateFin != null && log.getDateAction().isAfter(dateFin)) { + return false; + } + return true; + }) + .collect(Collectors.groupingBy( + AuditLogDTO::getActeurUsername, + Collectors.counting() + )); + } + + @Override + public long getFailureCount(LocalDateTime dateDebut, LocalDateTime dateFin) { + log.debug("Comptage des échecs entre {} et {}", dateDebut, dateFin); + + return auditLogs.values().stream() + .filter(log -> { + if (log.isSucces()) { + return false; // On ne compte que les échecs + } + if (dateDebut != null && log.getDateAction().isBefore(dateDebut)) { + return false; + } + if (dateFin != null && log.getDateAction().isAfter(dateFin)) { + return false; + } + return true; + }) + .count(); + } + + @Override + public long getSuccessCount(LocalDateTime dateDebut, LocalDateTime dateFin) { + log.debug("Comptage des succès entre {} et {}", dateDebut, dateFin); + + return auditLogs.values().stream() + .filter(log -> { + if (!log.isSucces()) { + return false; // On ne compte que les succès + } + if (dateDebut != null && log.getDateAction().isBefore(dateDebut)) { + return false; + } + if (dateFin != null && log.getDateAction().isAfter(dateFin)) { + return false; + } + return true; + }) + .count(); + } + + @Override + public List exportLogsToCSV(LocalDateTime dateDebut, LocalDateTime dateFin) { + log.info("Export CSV des logs d'audit entre {} et {}", dateDebut, dateFin); + + List csvLines = new ArrayList<>(); + + // En-tête CSV + csvLines.add("ID,Date Action,Acteur,Type Action,Ressource Type,Ressource ID,Succès,Adresse IP,Détails,Message Erreur"); + + // Données + auditLogs.values().stream() + .filter(log -> { + if (dateDebut != null && log.getDateAction().isBefore(dateDebut)) { + return false; + } + if (dateFin != null && log.getDateAction().isAfter(dateFin)) { + return false; + } + return true; + }) + .sorted((a, b) -> a.getDateAction().compareTo(b.getDateAction())) + .forEach(log -> { + String csvLine = String.format("%s,%s,%s,%s,%s,%s,%s,%s,\"%s\",\"%s\"", + log.getId(), + log.getDateAction(), + log.getActeurUsername(), + log.getTypeAction(), + log.getRessourceType(), + log.getRessourceId(), + log.isSucces(), + log.getAdresseIp() != null ? log.getAdresseIp() : "", + log.getDetails() != null ? log.getDetails().replace("\"", "\"\"") : "", + log.getMessageErreur() != null ? log.getMessageErreur().replace("\"", "\"\"") : "" + ); + csvLines.add(csvLine); + }); + + log.info("Export CSV terminé: {} lignes", csvLines.size() - 1); + return csvLines; + } + + @Override + public void purgeOldLogs(int joursDAnc ienneté) { + log.info("Purge des logs d'audit de plus de {} jours", joursDAncienneté); + + LocalDateTime dateLimit = LocalDateTime.now().minusDays(joursDAncienneté); + + long beforeCount = auditLogs.size(); + auditLogs.entrySet().removeIf(entry -> + entry.getValue().getDateAction().isBefore(dateLimit) + ); + long afterCount = auditLogs.size(); + + log.info("Purge terminée: {} logs supprimés", beforeCount - afterCount); + + // TODO: Si base de données utilisée, exécuter: + // DELETE FROM audit_log WHERE date_action < :dateLimit + } + + // ==================== Méthodes utilitaires ==================== + + /** + * Retourne le nombre total de logs en mémoire + */ + public long getTotalCount() { + return auditLogs.size(); + } + + /** + * Vide tous les logs (ATTENTION: à utiliser uniquement en développement) + */ + public void clearAll() { + log.warn("ATTENTION: Suppression de tous les logs d'audit en mémoire"); + auditLogs.clear(); + } +} diff --git a/lions-user-manager-server-impl-quarkus/src/main/java/dev/lions/user/manager/service/impl/RoleServiceImpl.java b/lions-user-manager-server-impl-quarkus/src/main/java/dev/lions/user/manager/service/impl/RoleServiceImpl.java new file mode 100644 index 0000000..d465e4d --- /dev/null +++ b/lions-user-manager-server-impl-quarkus/src/main/java/dev/lions/user/manager/service/impl/RoleServiceImpl.java @@ -0,0 +1,609 @@ +package dev.lions.user.manager.service.impl; + +import dev.lions.user.manager.client.KeycloakAdminClient; +import dev.lions.user.manager.dto.role.RoleAssignmentDTO; +import dev.lions.user.manager.dto.role.RoleDTO; +import dev.lions.user.manager.enums.role.TypeRole; +import dev.lions.user.manager.mapper.RoleMapper; +import dev.lions.user.manager.service.RoleService; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.ws.rs.NotFoundException; +import lombok.extern.slf4j.Slf4j; +import org.keycloak.admin.client.resource.*; +import org.keycloak.representations.idm.RoleRepresentation; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +/** + * Implémentation du service de gestion des rôles Keycloak + */ +@ApplicationScoped +@Slf4j +public class RoleServiceImpl implements RoleService { + + @Inject + KeycloakAdminClient keycloakAdminClient; + + // ==================== CRUD Realm Roles ==================== + + @Override + public RoleDTO createRealmRole(@Valid @NotNull RoleDTO roleDTO, @NotBlank String realmName) { + log.info("Création du rôle realm: {} dans le realm: {}", roleDTO.getNom(), realmName); + + RolesResource rolesResource = keycloakAdminClient.getInstance() + .realm(realmName) + .roles(); + + // Vérifier si le rôle existe déjà + try { + rolesResource.get(roleDTO.getNom()).toRepresentation(); + throw new IllegalArgumentException("Le rôle " + roleDTO.getNom() + " existe déjà"); + } catch (NotFoundException e) { + // OK, le rôle n'existe pas + } + + RoleRepresentation roleRep = RoleMapper.toRepresentation(roleDTO); + rolesResource.create(roleRep); + + // Récupérer le rôle créé avec son ID + RoleRepresentation createdRole = rolesResource.get(roleDTO.getNom()).toRepresentation(); + return RoleMapper.toDTO(createdRole, realmName, TypeRole.REALM_ROLE); + } + + @Override + public Optional getRealmRoleById(@NotBlank String roleId, @NotBlank String realmName) { + log.debug("Récupération du rôle realm par ID: {} dans le realm: {}", roleId, realmName); + + try { + RolesResource rolesResource = keycloakAdminClient.getInstance() + .realm(realmName) + .roles(); + + // Keycloak ne permet pas de récupérer un rôle par ID directement, on doit lister tous les rôles + List roles = rolesResource.list(); + return roles.stream() + .filter(r -> r.getId().equals(roleId)) + .findFirst() + .map(r -> RoleMapper.toDTO(r, realmName, TypeRole.REALM_ROLE)); + } catch (Exception e) { + log.error("Erreur lors de la récupération du rôle realm {}", roleId, e); + return Optional.empty(); + } + } + + @Override + public Optional getRealmRoleByName(@NotBlank String roleName, @NotBlank String realmName) { + log.debug("Récupération du rôle realm par nom: {} dans le realm: {}", roleName, realmName); + + try { + RoleRepresentation roleRep = keycloakAdminClient.getInstance() + .realm(realmName) + .roles() + .get(roleName) + .toRepresentation(); + + return Optional.of(RoleMapper.toDTO(roleRep, realmName, TypeRole.REALM_ROLE)); + } catch (NotFoundException e) { + log.warn("Rôle realm {} non trouvé dans le realm {}", roleName, realmName); + return Optional.empty(); + } + } + + @Override + public RoleDTO updateRealmRole(@NotBlank String roleName, @Valid @NotNull RoleDTO roleDTO, + @NotBlank String realmName) { + log.info("Mise à jour du rôle realm: {} dans le realm: {}", roleName, realmName); + + RoleResource roleResource = keycloakAdminClient.getInstance() + .realm(realmName) + .roles() + .get(roleName); + + RoleRepresentation roleRep = roleResource.toRepresentation(); + + // Mettre à jour uniquement les champs modifiables + if (roleDTO.getDescription() != null) { + roleRep.setDescription(roleDTO.getDescription()); + } + + roleResource.update(roleRep); + + // Retourner le rôle mis à jour + return RoleMapper.toDTO(roleResource.toRepresentation(), realmName, TypeRole.REALM_ROLE); + } + + @Override + public void deleteRealmRole(@NotBlank String roleName, @NotBlank String realmName) { + log.info("Suppression du rôle realm: {} dans le realm: {}", roleName, realmName); + + keycloakAdminClient.getInstance() + .realm(realmName) + .roles() + .deleteRole(roleName); + } + + @Override + public List getAllRealmRoles(@NotBlank String realmName) { + log.debug("Récupération de tous les rôles realm du realm: {}", realmName); + + List roleReps = keycloakAdminClient.getInstance() + .realm(realmName) + .roles() + .list(); + + return RoleMapper.toDTOList(roleReps, realmName, TypeRole.REALM_ROLE); + } + + // ==================== CRUD Client Roles ==================== + + @Override + public RoleDTO createClientRole(@Valid @NotNull RoleDTO roleDTO, @NotBlank String clientId, + @NotBlank String realmName) { + log.info("Création du rôle client: {} pour le client: {} dans le realm: {}", + roleDTO.getNom(), clientId, realmName); + + ClientsResource clientsResource = keycloakAdminClient.getInstance() + .realm(realmName) + .clients(); + + // Trouver le client par clientId + List clients = + clientsResource.findByClientId(clientId); + + if (clients.isEmpty()) { + throw new IllegalArgumentException("Client " + clientId + " non trouvé"); + } + + String internalClientId = clients.get(0).getId(); + RolesResource rolesResource = clientsResource.get(internalClientId).roles(); + + // Vérifier si le rôle existe déjà + try { + rolesResource.get(roleDTO.getNom()).toRepresentation(); + throw new IllegalArgumentException("Le rôle " + roleDTO.getNom() + " existe déjà pour ce client"); + } catch (NotFoundException e) { + // OK, le rôle n'existe pas + } + + RoleRepresentation roleRep = RoleMapper.toRepresentation(roleDTO); + rolesResource.create(roleRep); + + // Récupérer le rôle créé + RoleRepresentation createdRole = rolesResource.get(roleDTO.getNom()).toRepresentation(); + RoleDTO result = RoleMapper.toDTO(createdRole, realmName, TypeRole.CLIENT_ROLE); + result.setClientId(clientId); + + return result; + } + + @Override + public Optional getClientRoleByName(@NotBlank String roleName, @NotBlank String clientId, + @NotBlank String realmName) { + log.debug("Récupération du rôle client: {} pour le client: {} dans le realm: {}", + roleName, clientId, realmName); + + try { + ClientsResource clientsResource = keycloakAdminClient.getInstance() + .realm(realmName) + .clients(); + + List clients = + clientsResource.findByClientId(clientId); + + if (clients.isEmpty()) { + return Optional.empty(); + } + + String internalClientId = clients.get(0).getId(); + RoleRepresentation roleRep = clientsResource.get(internalClientId) + .roles() + .get(roleName) + .toRepresentation(); + + RoleDTO roleDTO = RoleMapper.toDTO(roleRep, realmName, TypeRole.CLIENT_ROLE); + roleDTO.setClientId(clientId); + + return Optional.of(roleDTO); + } catch (NotFoundException e) { + log.warn("Rôle client {} non trouvé pour le client {} dans le realm {}", + roleName, clientId, realmName); + return Optional.empty(); + } + } + + @Override + public void deleteClientRole(@NotBlank String roleName, @NotBlank String clientId, + @NotBlank String realmName) { + log.info("Suppression du rôle client: {} pour le client: {} dans le realm: {}", + roleName, clientId, realmName); + + ClientsResource clientsResource = keycloakAdminClient.getInstance() + .realm(realmName) + .clients(); + + List clients = + clientsResource.findByClientId(clientId); + + if (clients.isEmpty()) { + throw new IllegalArgumentException("Client " + clientId + " non trouvé"); + } + + String internalClientId = clients.get(0).getId(); + clientsResource.get(internalClientId).roles().deleteRole(roleName); + } + + @Override + public List getAllClientRoles(@NotBlank String clientId, @NotBlank String realmName) { + log.debug("Récupération de tous les rôles du client: {} dans le realm: {}", clientId, realmName); + + ClientsResource clientsResource = keycloakAdminClient.getInstance() + .realm(realmName) + .clients(); + + List clients = + clientsResource.findByClientId(clientId); + + if (clients.isEmpty()) { + return List.of(); + } + + String internalClientId = clients.get(0).getId(); + List roleReps = clientsResource.get(internalClientId) + .roles() + .list(); + + List roles = RoleMapper.toDTOList(roleReps, realmName, TypeRole.CLIENT_ROLE); + roles.forEach(role -> role.setClientId(clientId)); + + return roles; + } + + // ==================== Attribution de rôles ==================== + + @Override + public void assignRealmRolesToUser(@NotBlank String userId, @NotNull List roleNames, + @NotBlank String realmName) { + log.info("Attribution de {} rôles realm à l'utilisateur {} dans le realm {}", + roleNames.size(), userId, realmName); + + UserResource userResource = keycloakAdminClient.getInstance() + .realm(realmName) + .users() + .get(userId); + + RolesResource rolesResource = keycloakAdminClient.getInstance() + .realm(realmName) + .roles(); + + List rolesToAssign = roleNames.stream() + .map(roleName -> { + try { + return rolesResource.get(roleName).toRepresentation(); + } catch (NotFoundException e) { + log.warn("Rôle {} non trouvé, ignoré", roleName); + return null; + } + }) + .filter(role -> role != null) + .collect(Collectors.toList()); + + if (!rolesToAssign.isEmpty()) { + userResource.roles().realmLevel().add(rolesToAssign); + } + } + + @Override + public void revokeRealmRolesFromUser(@NotBlank String userId, @NotNull List roleNames, + @NotBlank String realmName) { + log.info("Révocation de {} rôles realm pour l'utilisateur {} dans le realm {}", + roleNames.size(), userId, realmName); + + UserResource userResource = keycloakAdminClient.getInstance() + .realm(realmName) + .users() + .get(userId); + + RolesResource rolesResource = keycloakAdminClient.getInstance() + .realm(realmName) + .roles(); + + List rolesToRevoke = roleNames.stream() + .map(roleName -> { + try { + return rolesResource.get(roleName).toRepresentation(); + } catch (NotFoundException e) { + log.warn("Rôle {} non trouvé, ignoré", roleName); + return null; + } + }) + .filter(role -> role != null) + .collect(Collectors.toList()); + + if (!rolesToRevoke.isEmpty()) { + userResource.roles().realmLevel().remove(rolesToRevoke); + } + } + + @Override + public void assignClientRolesToUser(@NotBlank String userId, @NotBlank String clientId, + @NotNull List roleNames, @NotBlank String realmName) { + log.info("Attribution de {} rôles du client {} à l'utilisateur {} dans le realm {}", + roleNames.size(), clientId, userId, realmName); + + UserResource userResource = keycloakAdminClient.getInstance() + .realm(realmName) + .users() + .get(userId); + + ClientsResource clientsResource = keycloakAdminClient.getInstance() + .realm(realmName) + .clients(); + + List clients = + clientsResource.findByClientId(clientId); + + if (clients.isEmpty()) { + throw new IllegalArgumentException("Client " + clientId + " non trouvé"); + } + + String internalClientId = clients.get(0).getId(); + RolesResource rolesResource = clientsResource.get(internalClientId).roles(); + + List rolesToAssign = roleNames.stream() + .map(roleName -> { + try { + return rolesResource.get(roleName).toRepresentation(); + } catch (NotFoundException e) { + log.warn("Rôle client {} non trouvé, ignoré", roleName); + return null; + } + }) + .filter(role -> role != null) + .collect(Collectors.toList()); + + if (!rolesToAssign.isEmpty()) { + userResource.roles().clientLevel(internalClientId).add(rolesToAssign); + } + } + + @Override + public void revokeClientRolesFromUser(@NotBlank String userId, @NotBlank String clientId, + @NotNull List roleNames, @NotBlank String realmName) { + log.info("Révocation de {} rôles du client {} pour l'utilisateur {} dans le realm {}", + roleNames.size(), clientId, userId, realmName); + + UserResource userResource = keycloakAdminClient.getInstance() + .realm(realmName) + .users() + .get(userId); + + ClientsResource clientsResource = keycloakAdminClient.getInstance() + .realm(realmName) + .clients(); + + List clients = + clientsResource.findByClientId(clientId); + + if (clients.isEmpty()) { + throw new IllegalArgumentException("Client " + clientId + " non trouvé"); + } + + String internalClientId = clients.get(0).getId(); + RolesResource rolesResource = clientsResource.get(internalClientId).roles(); + + List rolesToRevoke = roleNames.stream() + .map(roleName -> { + try { + return rolesResource.get(roleName).toRepresentation(); + } catch (NotFoundException e) { + log.warn("Rôle client {} non trouvé, ignoré", roleName); + return null; + } + }) + .filter(role -> role != null) + .collect(Collectors.toList()); + + if (!rolesToRevoke.isEmpty()) { + userResource.roles().clientLevel(internalClientId).remove(rolesToRevoke); + } + } + + @Override + public List getUserRealmRoles(@NotBlank String userId, @NotBlank String realmName) { + log.debug("Récupération des rôles realm de l'utilisateur {} dans le realm {}", userId, realmName); + + List roleReps = keycloakAdminClient.getInstance() + .realm(realmName) + .users() + .get(userId) + .roles() + .realmLevel() + .listAll(); + + return RoleMapper.toDTOList(roleReps, realmName, TypeRole.REALM_ROLE); + } + + @Override + public List getUserClientRoles(@NotBlank String userId, @NotBlank String clientId, + @NotBlank String realmName) { + log.debug("Récupération des rôles du client {} pour l'utilisateur {} dans le realm {}", + clientId, userId, realmName); + + ClientsResource clientsResource = keycloakAdminClient.getInstance() + .realm(realmName) + .clients(); + + List clients = + clientsResource.findByClientId(clientId); + + if (clients.isEmpty()) { + return List.of(); + } + + String internalClientId = clients.get(0).getId(); + List roleReps = keycloakAdminClient.getInstance() + .realm(realmName) + .users() + .get(userId) + .roles() + .clientLevel(internalClientId) + .listAll(); + + List roles = RoleMapper.toDTOList(roleReps, realmName, TypeRole.CLIENT_ROLE); + roles.forEach(role -> role.setClientId(clientId)); + + return roles; + } + + // ==================== Rôles composites ==================== + + @Override + public void addCompositesToRealmRole(@NotBlank String roleName, @NotNull List compositeRoleNames, + @NotBlank String realmName) { + log.info("Ajout de {} rôles composites au rôle realm {} dans le realm {}", + compositeRoleNames.size(), roleName, realmName); + + RoleResource roleResource = keycloakAdminClient.getInstance() + .realm(realmName) + .roles() + .get(roleName); + + RolesResource rolesResource = keycloakAdminClient.getInstance() + .realm(realmName) + .roles(); + + List compositesToAdd = compositeRoleNames.stream() + .map(compositeName -> { + try { + return rolesResource.get(compositeName).toRepresentation(); + } catch (NotFoundException e) { + log.warn("Rôle composite {} non trouvé, ignoré", compositeName); + return null; + } + }) + .filter(role -> role != null) + .collect(Collectors.toList()); + + if (!compositesToAdd.isEmpty()) { + roleResource.addComposites(compositesToAdd); + } + } + + @Override + public void removeCompositesFromRealmRole(@NotBlank String roleName, @NotNull List compositeRoleNames, + @NotBlank String realmName) { + log.info("Suppression de {} rôles composites du rôle realm {} dans le realm {}", + compositeRoleNames.size(), roleName, realmName); + + RoleResource roleResource = keycloakAdminClient.getInstance() + .realm(realmName) + .roles() + .get(roleName); + + RolesResource rolesResource = keycloakAdminClient.getInstance() + .realm(realmName) + .roles(); + + List compositesToRemove = compositeRoleNames.stream() + .map(compositeName -> { + try { + return rolesResource.get(compositeName).toRepresentation(); + } catch (NotFoundException e) { + log.warn("Rôle composite {} non trouvé, ignoré", compositeName); + return null; + } + }) + .filter(role -> role != null) + .collect(Collectors.toList()); + + if (!compositesToRemove.isEmpty()) { + roleResource.deleteComposites(compositesToRemove); + } + } + + @Override + public List getCompositeRoles(@NotBlank String roleName, @NotBlank String realmName) { + log.debug("Récupération des rôles composites du rôle {} dans le realm {}", roleName, realmName); + + List composites = keycloakAdminClient.getInstance() + .realm(realmName) + .roles() + .get(roleName) + .getRoleComposites(); + + return RoleMapper.toDTOList(composites, realmName, TypeRole.COMPOSITE_ROLE); + } + + // ==================== Vérification de permissions ==================== + + @Override + public boolean userHasRealmRole(@NotBlank String userId, @NotBlank String roleName, + @NotBlank String realmName) { + log.debug("Vérification si l'utilisateur {} a le rôle realm {} dans le realm {}", + userId, roleName, realmName); + + List userRoles = keycloakAdminClient.getInstance() + .realm(realmName) + .users() + .get(userId) + .roles() + .realmLevel() + .listEffective(); // Incluant les rôles hérités via composites + + return userRoles.stream() + .anyMatch(role -> role.getName().equals(roleName)); + } + + @Override + public boolean userHasClientRole(@NotBlank String userId, @NotBlank String clientId, + @NotBlank String roleName, @NotBlank String realmName) { + log.debug("Vérification si l'utilisateur {} a le rôle client {} du client {} dans le realm {}", + userId, roleName, clientId, realmName); + + ClientsResource clientsResource = keycloakAdminClient.getInstance() + .realm(realmName) + .clients(); + + List clients = + clientsResource.findByClientId(clientId); + + if (clients.isEmpty()) { + return false; + } + + String internalClientId = clients.get(0).getId(); + List userClientRoles = keycloakAdminClient.getInstance() + .realm(realmName) + .users() + .get(userId) + .roles() + .clientLevel(internalClientId) + .listEffective(); + + return userClientRoles.stream() + .anyMatch(role -> role.getName().equals(roleName)); + } + + @Override + public List getUserEffectiveRealmRoles(@NotBlank String userId, @NotBlank String realmName) { + log.debug("Récupération des rôles realm effectifs de l'utilisateur {} dans le realm {}", + userId, realmName); + + List effectiveRoles = keycloakAdminClient.getInstance() + .realm(realmName) + .users() + .get(userId) + .roles() + .realmLevel() + .listEffective(); + + return RoleMapper.toDTOList(effectiveRoles, realmName, TypeRole.REALM_ROLE); + } +}