Compare commits
12 Commits
45ae712c0f
...
e0f5bba399
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e0f5bba399 | ||
|
|
bbab8ca7ec | ||
|
|
bf1e9e16d8 | ||
|
|
3773fac0b0 | ||
|
|
2bc1b0f6a5 | ||
|
|
03984b50c9 | ||
|
|
a87dca37d8 | ||
|
|
96c873c36b | ||
|
|
786a928c8c | ||
|
|
ca6a7bdddd | ||
|
|
b259a9232d | ||
|
|
54471a3f90 |
246
cleanup.ps1
Normal file
246
cleanup.ps1
Normal file
@@ -0,0 +1,246 @@
|
||||
#!/usr/bin/env pwsh
|
||||
# ============================================================================
|
||||
# Lions User Manager - Script de nettoyage complet
|
||||
# ============================================================================
|
||||
# Ce script nettoie le projet selon les meilleures pratiques :
|
||||
# 1. Supprime les fichiers parasites (crash JVM, test output, tokens)
|
||||
# 2. Supprime les répertoires target Maven
|
||||
# 3. Consolide la documentation dans docs/
|
||||
# 4. Supprime les scripts dupliqués de la racine
|
||||
# 5. Nettoie les répertoires Java vides
|
||||
# 6. Supprime les répertoires squelettes vides
|
||||
# ============================================================================
|
||||
|
||||
$ErrorActionPreference = "SilentlyContinue"
|
||||
$root = $PSScriptRoot
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "======================================================" -ForegroundColor Cyan
|
||||
Write-Host " Lions User Manager - Nettoyage Complet du Projet" -ForegroundColor Cyan
|
||||
Write-Host "======================================================" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
|
||||
# ============================================
|
||||
# 1. Suppression des fichiers parasites
|
||||
# ============================================
|
||||
Write-Host "1. Suppression des fichiers parasites..." -ForegroundColor Yellow
|
||||
|
||||
$parasiteFiles = @(
|
||||
# Racine
|
||||
"$root\token.json",
|
||||
"$root\token.txt",
|
||||
"$root\clean-project.ps1",
|
||||
|
||||
# server-api
|
||||
"$root\lions-user-manager-server-api\test_output.txt",
|
||||
"$root\lions-user-manager-server-api\test_output_2.txt",
|
||||
"$root\lions-user-manager-server-api\test_output_3.txt",
|
||||
|
||||
# server-impl
|
||||
"$root\lions-user-manager-server-impl-quarkus\hs_err_pid29312.log",
|
||||
"$root\lions-user-manager-server-impl-quarkus\hs_err_pid64388.log",
|
||||
"$root\lions-user-manager-server-impl-quarkus\test_output.txt",
|
||||
"$root\lions-user-manager-server-impl-quarkus\create_database.sql",
|
||||
|
||||
# client
|
||||
"$root\lions-user-manager-client-quarkus-primefaces-freya\hs_err_pid131240.log",
|
||||
"$root\lions-user-manager-client-quarkus-primefaces-freya\hs_err_pid31092.log",
|
||||
"$root\lions-user-manager-client-quarkus-primefaces-freya\hs_err_pid86240.log",
|
||||
"$root\lions-user-manager-client-quarkus-primefaces-freya\replay_pid31092.log"
|
||||
)
|
||||
|
||||
foreach ($file in $parasiteFiles) {
|
||||
if (Test-Path $file) {
|
||||
Remove-Item $file -Force
|
||||
Write-Host " Supprime: $(Split-Path $file -Leaf)" -ForegroundColor Green
|
||||
}
|
||||
}
|
||||
|
||||
# ============================================
|
||||
# 2. Suppression des répertoires target Maven
|
||||
# ============================================
|
||||
Write-Host ""
|
||||
Write-Host "2. Suppression des répertoires target Maven..." -ForegroundColor Yellow
|
||||
|
||||
$targetDirs = @(
|
||||
"$root\lions-user-manager-server-api\target",
|
||||
"$root\lions-user-manager-server-impl-quarkus\target",
|
||||
"$root\lions-user-manager-client-quarkus-primefaces-freya\target"
|
||||
)
|
||||
|
||||
foreach ($dir in $targetDirs) {
|
||||
if (Test-Path $dir) {
|
||||
Remove-Item $dir -Recurse -Force
|
||||
Write-Host " Supprime: $($dir.Replace($root, '.'))" -ForegroundColor Green
|
||||
}
|
||||
}
|
||||
|
||||
# Suppression du répertoire logs du serveur
|
||||
$logsDir = "$root\lions-user-manager-server-impl-quarkus\logs"
|
||||
if (Test-Path $logsDir) {
|
||||
Remove-Item $logsDir -Recurse -Force
|
||||
Write-Host " Supprime: .\lions-user-manager-server-impl-quarkus\logs" -ForegroundColor Green
|
||||
}
|
||||
|
||||
# ============================================
|
||||
# 3. Suppression des fichiers .md parasites à la racine
|
||||
# ============================================
|
||||
Write-Host ""
|
||||
Write-Host "3. Suppression des fichiers .md parasites à la racine..." -ForegroundColor Yellow
|
||||
|
||||
# Fichiers .md à GARDER à la racine
|
||||
$keepMd = @("README.md")
|
||||
|
||||
Get-ChildItem "$root\*.md" -File | Where-Object {
|
||||
$keepMd -notcontains $_.Name
|
||||
} | ForEach-Object {
|
||||
Remove-Item $_.FullName -Force
|
||||
Write-Host " Supprime: $($_.Name)" -ForegroundColor Green
|
||||
}
|
||||
|
||||
# Suppression du fichier SUMMARY_VISUAL.txt
|
||||
if (Test-Path "$root\SUMMARY_VISUAL.txt") {
|
||||
Remove-Item "$root\SUMMARY_VISUAL.txt" -Force
|
||||
Write-Host " Supprime: SUMMARY_VISUAL.txt" -ForegroundColor Green
|
||||
}
|
||||
|
||||
# ============================================
|
||||
# 4. Nettoyage des scripts dupliqués à la racine
|
||||
# ============================================
|
||||
Write-Host ""
|
||||
Write-Host "4. Nettoyage des scripts dupliqués de la racine..." -ForegroundColor Yellow
|
||||
|
||||
# Scripts à supprimer de la racine (doublons ou ponctuels)
|
||||
$rootScriptsToRemove = @(
|
||||
"$root\configure-keycloak-frontend.sh",
|
||||
"$root\configure-keycloak-test-user.ps1",
|
||||
"$root\configure-keycloak-test-user.sh",
|
||||
"$root\create-roles-and-assign.sh",
|
||||
"$root\get-client-secret.sh",
|
||||
"$root\integrate-freya-extension.ps1",
|
||||
"$root\migrate-all-xhtml-to-freya.ps1",
|
||||
"$root\setup-keycloak-client.ps1",
|
||||
"$root\setup-keycloak-correct.sh",
|
||||
"$root\setup-keycloak-simple.sh",
|
||||
"$root\setup-keycloak.sh",
|
||||
"$root\test-auth.sh",
|
||||
"$root\test-keycloak-config.ps1",
|
||||
"$root\test-keycloak-config.sh",
|
||||
"$root\test-keycloak-token.ps1",
|
||||
"$root\update-client-config.ps1",
|
||||
"$root\verify-client-config.ps1"
|
||||
)
|
||||
|
||||
foreach ($script in $rootScriptsToRemove) {
|
||||
if (Test-Path $script) {
|
||||
Remove-Item $script -Force
|
||||
Write-Host " Supprime: $(Split-Path $script -Leaf)" -ForegroundColor Green
|
||||
}
|
||||
}
|
||||
|
||||
# ============================================
|
||||
# 5. Suppression des répertoires vides
|
||||
# ============================================
|
||||
Write-Host ""
|
||||
Write-Host "5. Suppression des répertoires vides..." -ForegroundColor Yellow
|
||||
|
||||
$emptyDirs = @(
|
||||
# Répertoires squelettes vides
|
||||
"$root\docs",
|
||||
"$root\helm",
|
||||
"$root\tests",
|
||||
|
||||
# Répertoires Java vides dans le client
|
||||
"$root\lions-user-manager-client-quarkus-primefaces-freya\src\main\java\dev\lions\user\manager\client\config",
|
||||
"$root\lions-user-manager-client-quarkus-primefaces-freya\src\main\java\dev\lions\user\manager\client\dto",
|
||||
"$root\lions-user-manager-client-quarkus-primefaces-freya\src\main\java\dev\lions\user\manager\client\exception",
|
||||
"$root\lions-user-manager-client-quarkus-primefaces-freya\src\main\java\dev\lions\user\manager\client\security",
|
||||
|
||||
# Répertoires Java vides dans le serveur
|
||||
"$root\lions-user-manager-server-impl-quarkus\src\main\java\dev\lions\user\manager\server\client",
|
||||
"$root\lions-user-manager-server-impl-quarkus\src\main\java\dev\lions\user\manager\server\repository",
|
||||
"$root\lions-user-manager-server-impl-quarkus\src\main\java\dev\lions\user\manager\server\resource",
|
||||
"$root\lions-user-manager-server-impl-quarkus\src\main\java\dev\lions\user\manager\server\security",
|
||||
"$root\lions-user-manager-server-impl-quarkus\src\main\java\dev\lions\user\manager\server\service"
|
||||
)
|
||||
|
||||
foreach ($dir in $emptyDirs) {
|
||||
if (Test-Path $dir) {
|
||||
$children = Get-ChildItem $dir -Recurse -File
|
||||
if ($null -eq $children -or $children.Count -eq 0) {
|
||||
Remove-Item $dir -Recurse -Force
|
||||
Write-Host " Supprime: $($dir.Replace($root, '.'))" -ForegroundColor Green
|
||||
}
|
||||
else {
|
||||
Write-Host " GARDE (non vide): $($dir.Replace($root, '.'))" -ForegroundColor DarkYellow
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ============================================
|
||||
# 6. Suppression des .git dans les sous-modules
|
||||
# ============================================
|
||||
Write-Host ""
|
||||
Write-Host "6. Nettoyage des .git dans les sous-modules..." -ForegroundColor Yellow
|
||||
|
||||
$submoduleGits = @(
|
||||
"$root\lions-user-manager-server-api\.git",
|
||||
"$root\lions-user-manager-client-quarkus-primefaces-freya\.git",
|
||||
"$root\lions-user-manager-server-impl-quarkus\.git"
|
||||
)
|
||||
|
||||
foreach ($gitFile in $submoduleGits) {
|
||||
if (Test-Path $gitFile -PathType Leaf) {
|
||||
Remove-Item $gitFile -Force
|
||||
Write-Host " Supprime sous-module .git: $(Split-Path (Split-Path $gitFile) -Leaf)" -ForegroundColor Green
|
||||
}
|
||||
elseif (Test-Path $gitFile -PathType Container) {
|
||||
Remove-Item $gitFile -Recurse -Force
|
||||
Write-Host " Supprime sous-module .git/: $(Split-Path (Split-Path $gitFile) -Leaf)" -ForegroundColor Green
|
||||
}
|
||||
}
|
||||
|
||||
# ============================================
|
||||
# 7. Suppression des .gitignore redondants dans les sous-modules
|
||||
# ============================================
|
||||
Write-Host ""
|
||||
Write-Host "7. Suppression des .gitignore redondants..." -ForegroundColor Yellow
|
||||
|
||||
$submoduleGitignores = @(
|
||||
"$root\lions-user-manager-server-api\.gitignore",
|
||||
"$root\lions-user-manager-server-impl-quarkus\.gitignore",
|
||||
"$root\lions-user-manager-client-quarkus-primefaces-freya\.gitignore"
|
||||
)
|
||||
|
||||
foreach ($gi in $submoduleGitignores) {
|
||||
if (Test-Path $gi) {
|
||||
Remove-Item $gi -Force
|
||||
Write-Host " Supprime: $($gi.Replace($root, '.'))" -ForegroundColor Green
|
||||
}
|
||||
}
|
||||
|
||||
# ============================================
|
||||
# Résumé
|
||||
# ============================================
|
||||
Write-Host ""
|
||||
Write-Host "======================================================" -ForegroundColor Cyan
|
||||
Write-Host " Nettoyage terminé !" -ForegroundColor Green
|
||||
Write-Host "======================================================" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
Write-Host "Structure finale attendue :" -ForegroundColor White
|
||||
Write-Host " lions-user-manager/" -ForegroundColor White
|
||||
Write-Host " ├── .git/" -ForegroundColor DarkGray
|
||||
Write-Host " ├── .gitignore" -ForegroundColor DarkGray
|
||||
Write-Host " ├── README.md" -ForegroundColor White
|
||||
Write-Host " ├── pom.xml" -ForegroundColor White
|
||||
Write-Host " ├── scripts/" -ForegroundColor Cyan
|
||||
Write-Host " ├── restart-dev.ps1" -ForegroundColor White
|
||||
Write-Host " ├── start-backend.bat" -ForegroundColor White
|
||||
Write-Host " ├── start-client.bat" -ForegroundColor White
|
||||
Write-Host " ├── Dockerfile.prod (dans chaque module)" -ForegroundColor White
|
||||
Write-Host " ├── lions-user-manager-server-api/" -ForegroundColor Yellow
|
||||
Write-Host " ├── lions-user-manager-server-impl-quarkus/" -ForegroundColor Yellow
|
||||
Write-Host " └── lions-user-manager-client-quarkus-primefaces-freya/" -ForegroundColor Yellow
|
||||
Write-Host ""
|
||||
Write-Host "Prochaine étape : 'mvn clean compile' pour vérifier la compilation" -ForegroundColor Magenta
|
||||
Write-Host ""
|
||||
6
lombok.config
Normal file
6
lombok.config
Normal file
@@ -0,0 +1,6 @@
|
||||
# This file configures Lombok for the project
|
||||
# See https://projectlombok.org/features/configuration
|
||||
|
||||
# Add @Generated annotation to all generated code
|
||||
# This allows JaCoCo to automatically exclude Lombok-generated code from coverage
|
||||
lombok.addLombokGeneratedAnnotation = true
|
||||
8
pom.xml
8
pom.xml
@@ -80,6 +80,8 @@
|
||||
<artifactId>quarkus-keycloak-admin-rest-client</artifactId>
|
||||
</dependency>
|
||||
|
||||
|
||||
|
||||
<!-- Optional: Database for audit logs -->
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
@@ -141,6 +143,12 @@
|
||||
<artifactId>mockito-core</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-junit-jupiter</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
||||
13
script/docker/.env.example
Normal file
13
script/docker/.env.example
Normal file
@@ -0,0 +1,13 @@
|
||||
# Base de données
|
||||
DB_NAME=lions_user_manager
|
||||
DB_USER=lions
|
||||
DB_PASSWORD=lions
|
||||
DB_PORT=5432
|
||||
|
||||
# Keycloak
|
||||
KC_ADMIN=admin
|
||||
KC_ADMIN_PASSWORD=admin
|
||||
KC_PORT=8180
|
||||
|
||||
# Serveur
|
||||
SERVER_PORT=8080
|
||||
35
script/docker/dependencies-docker-compose.yml
Normal file
35
script/docker/dependencies-docker-compose.yml
Normal file
@@ -0,0 +1,35 @@
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:15
|
||||
environment:
|
||||
POSTGRES_DB: ${DB_NAME:-lions_user_manager}
|
||||
POSTGRES_USER: ${DB_USER:-lions}
|
||||
POSTGRES_PASSWORD: ${DB_PASSWORD:-lions}
|
||||
ports:
|
||||
- "${DB_PORT:-5432}:5432"
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-lions} -d ${DB_NAME:-lions_user_manager}"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
keycloak:
|
||||
image: quay.io/keycloak/keycloak:26.3.3
|
||||
command: start-dev
|
||||
environment:
|
||||
KC_DB: postgres
|
||||
KC_DB_URL: jdbc:postgresql://postgres:5432/${DB_NAME:-lions_user_manager}
|
||||
KC_DB_USERNAME: ${DB_USER:-lions}
|
||||
KC_DB_PASSWORD: ${DB_PASSWORD:-lions}
|
||||
KC_BOOTSTRAP_ADMIN_USERNAME: ${KC_ADMIN:-admin}
|
||||
KC_BOOTSTRAP_ADMIN_PASSWORD: ${KC_ADMIN_PASSWORD:-admin}
|
||||
ports:
|
||||
- "${KC_PORT:-8180}:8080"
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
52
script/docker/docker-compose.yml
Normal file
52
script/docker/docker-compose.yml
Normal file
@@ -0,0 +1,52 @@
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:15
|
||||
environment:
|
||||
POSTGRES_DB: ${DB_NAME:-lions_user_manager}
|
||||
POSTGRES_USER: ${DB_USER:-lions}
|
||||
POSTGRES_PASSWORD: ${DB_PASSWORD:-lions}
|
||||
ports:
|
||||
- "${DB_PORT:-5432}:5432"
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-lions} -d ${DB_NAME:-lions_user_manager}"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
keycloak:
|
||||
image: quay.io/keycloak/keycloak:26.3.3
|
||||
command: start-dev
|
||||
environment:
|
||||
KC_DB: postgres
|
||||
KC_DB_URL: jdbc:postgresql://postgres:5432/${DB_NAME:-lions_user_manager}
|
||||
KC_DB_USERNAME: ${DB_USER:-lions}
|
||||
KC_DB_PASSWORD: ${DB_PASSWORD:-lions}
|
||||
KC_BOOTSTRAP_ADMIN_USERNAME: ${KC_ADMIN:-admin}
|
||||
KC_BOOTSTRAP_ADMIN_PASSWORD: ${KC_ADMIN_PASSWORD:-admin}
|
||||
ports:
|
||||
- "${KC_PORT:-8180}:8080"
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
|
||||
lions-user-manager-server:
|
||||
build:
|
||||
context: ../..
|
||||
dockerfile: src/main/docker/Dockerfile.jvm
|
||||
ports:
|
||||
- "${SERVER_PORT:-8080}:8080"
|
||||
environment:
|
||||
QUARKUS_DATASOURCE_JDBC_URL: jdbc:postgresql://postgres:5432/${DB_NAME:-lions_user_manager}
|
||||
QUARKUS_DATASOURCE_USERNAME: ${DB_USER:-lions}
|
||||
QUARKUS_DATASOURCE_PASSWORD: ${DB_PASSWORD:-lions}
|
||||
KEYCLOAK_SERVER_URL: http://keycloak:8080
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
keycloak:
|
||||
condition: service_started
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
5
script/docker/run-dev.bat
Normal file
5
script/docker/run-dev.bat
Normal file
@@ -0,0 +1,5 @@
|
||||
@echo off
|
||||
REM Demarre les dependances (postgres + keycloak) puis le serveur en mode dev (mvn quarkus:dev -P dev)
|
||||
cd /d "%~dp0\..\.."
|
||||
docker-compose -f script/docker/dependencies-docker-compose.yml up -d
|
||||
mvn quarkus:dev -P dev
|
||||
7
script/docker/run-dev.sh
Normal file
7
script/docker/run-dev.sh
Normal file
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
# Démarre les dépendances (postgres + keycloak) puis le serveur en mode dev (mvn quarkus:dev -P dev)
|
||||
set -e
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$SCRIPT_DIR/../.."
|
||||
docker-compose -f script/docker/dependencies-docker-compose.yml up -d
|
||||
mvn quarkus:dev -P dev
|
||||
11
src/main/docker/Dockerfile.jvm
Normal file
11
src/main/docker/Dockerfile.jvm
Normal file
@@ -0,0 +1,11 @@
|
||||
FROM registry.access.redhat.com/ubi8/openjdk-17:1.20
|
||||
ENV LANGUAGE='en_US:en'
|
||||
COPY --chown=185 target/quarkus-app/lib/ /deployments/lib/
|
||||
COPY --chown=185 target/quarkus-app/*.jar /deployments/
|
||||
COPY --chown=185 target/quarkus-app/app/ /deployments/app/
|
||||
COPY --chown=185 target/quarkus-app/quarkus/ /deployments/quarkus/
|
||||
EXPOSE 8080
|
||||
USER 185
|
||||
ENV JAVA_OPTS_APPEND="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager"
|
||||
ENV JAVA_APP_JAR="/deployments/quarkus-run.jar"
|
||||
ENTRYPOINT ["/opt/jboss/container/java/run/run-java.sh"]
|
||||
@@ -51,6 +51,19 @@ public interface KeycloakAdminClient {
|
||||
*/
|
||||
boolean realmExists(String realmName);
|
||||
|
||||
/**
|
||||
* Récupère la liste de tous les realms
|
||||
* @return Liste des noms de realms
|
||||
*/
|
||||
java.util.List<String> getAllRealms();
|
||||
|
||||
/**
|
||||
* Récupère la liste des clientId d'un realm
|
||||
* @param realmName nom du realm
|
||||
* @return Liste des clientId
|
||||
*/
|
||||
java.util.List<String> getRealmClients(String realmName);
|
||||
|
||||
/**
|
||||
* Ferme la connexion Keycloak
|
||||
*/
|
||||
|
||||
@@ -1,27 +1,37 @@
|
||||
package dev.lions.user.manager.client;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.quarkus.runtime.Startup;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.PreDestroy;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||
import org.eclipse.microprofile.faulttolerance.CircuitBreaker;
|
||||
import org.eclipse.microprofile.faulttolerance.Retry;
|
||||
import org.eclipse.microprofile.faulttolerance.Timeout;
|
||||
import org.keycloak.admin.client.Keycloak;
|
||||
import org.keycloak.admin.client.KeycloakBuilder;
|
||||
import org.keycloak.admin.client.resource.RealmResource;
|
||||
import org.keycloak.admin.client.resource.RolesResource;
|
||||
import org.keycloak.admin.client.resource.UsersResource;
|
||||
|
||||
import jakarta.ws.rs.NotFoundException;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Implémentation du client Keycloak Admin
|
||||
* Utilise le bean Keycloak géré par Quarkus (quarkus-keycloak-admin-rest-client)
|
||||
* qui respecte la configuration Jackson (fail-on-unknown-properties=false)
|
||||
* Utilise Circuit Breaker, Retry et Timeout pour la résilience
|
||||
*/
|
||||
@ApplicationScoped
|
||||
@@ -29,6 +39,9 @@ import java.time.temporal.ChronoUnit;
|
||||
@Slf4j
|
||||
public class KeycloakAdminClientImpl implements KeycloakAdminClient {
|
||||
|
||||
@Inject
|
||||
Keycloak keycloak;
|
||||
|
||||
@ConfigProperty(name = "lions.keycloak.server-url")
|
||||
String serverUrl;
|
||||
|
||||
@@ -41,17 +54,6 @@ public class KeycloakAdminClientImpl implements KeycloakAdminClient {
|
||||
@ConfigProperty(name = "lions.keycloak.admin-username")
|
||||
String adminUsername;
|
||||
|
||||
@ConfigProperty(name = "lions.keycloak.admin-password")
|
||||
String adminPassword;
|
||||
|
||||
@ConfigProperty(name = "lions.keycloak.connection-pool-size", defaultValue = "10")
|
||||
Integer connectionPoolSize;
|
||||
|
||||
@ConfigProperty(name = "lions.keycloak.timeout-seconds", defaultValue = "30")
|
||||
Integer timeoutSeconds;
|
||||
|
||||
private Keycloak keycloak;
|
||||
|
||||
@PostConstruct
|
||||
void init() {
|
||||
log.info("========================================");
|
||||
@@ -61,29 +63,8 @@ public class KeycloakAdminClientImpl implements KeycloakAdminClient {
|
||||
log.info("Admin Realm: {}", adminRealm);
|
||||
log.info("Admin Client ID: {}", adminClientId);
|
||||
log.info("Admin Username: {}", adminUsername);
|
||||
log.info("Connection Pool Size: {}", connectionPoolSize);
|
||||
log.info("Timeout: {} secondes", timeoutSeconds);
|
||||
|
||||
try {
|
||||
this.keycloak = KeycloakBuilder.builder()
|
||||
.serverUrl(serverUrl)
|
||||
.realm(adminRealm)
|
||||
.clientId(adminClientId)
|
||||
.username(adminUsername)
|
||||
.password(adminPassword)
|
||||
.build();
|
||||
|
||||
log.info("✅ Client Keycloak initialisé (connexion lazy)");
|
||||
log.info("La connexion sera établie lors de la première requête API");
|
||||
} catch (Exception e) {
|
||||
log.warn("⚠️ Échec de l'initialisation du client Keycloak");
|
||||
log.warn("URL: {}", serverUrl);
|
||||
log.warn("Realm: {}", adminRealm);
|
||||
log.warn("Username: {}", adminUsername);
|
||||
log.warn("Message: {}", e.getMessage());
|
||||
// Ne pas bloquer le démarrage - la connexion sera tentée lors du premier appel
|
||||
this.keycloak = null;
|
||||
}
|
||||
log.info("✅ Client Keycloak initialisé via Quarkus CDI (connexion lazy)");
|
||||
log.info("La connexion sera établie lors de la première requête API");
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -91,10 +72,6 @@ public class KeycloakAdminClientImpl implements KeycloakAdminClient {
|
||||
@Timeout(value = 30, unit = ChronoUnit.SECONDS)
|
||||
@CircuitBreaker(requestVolumeThreshold = 10, failureRatio = 0.5, delay = 5000)
|
||||
public Keycloak getInstance() {
|
||||
if (keycloak == null) {
|
||||
log.warn("Instance Keycloak null, tentative de réinitialisation...");
|
||||
init();
|
||||
}
|
||||
return keycloak;
|
||||
}
|
||||
|
||||
@@ -104,7 +81,7 @@ public class KeycloakAdminClientImpl implements KeycloakAdminClient {
|
||||
@CircuitBreaker(requestVolumeThreshold = 10, failureRatio = 0.5, delay = 5000)
|
||||
public RealmResource getRealm(String realmName) {
|
||||
try {
|
||||
return getInstance().realm(realmName);
|
||||
return keycloak.realm(realmName);
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors de la récupération du realm {}: {}", realmName, e.getMessage());
|
||||
throw new RuntimeException("Impossible de récupérer le realm: " + realmName, e);
|
||||
@@ -130,10 +107,9 @@ public class KeycloakAdminClientImpl implements KeycloakAdminClient {
|
||||
@Override
|
||||
public boolean isConnected() {
|
||||
try {
|
||||
if (keycloak == null) {
|
||||
return false;
|
||||
}
|
||||
keycloak.serverInfo().getInfo();
|
||||
// getAccessTokenString() n'implique pas la désérialisation de ServerInfoRepresentation
|
||||
// (qui échoue sur le champ inconnu "cpuInfo" avec Keycloak 26+)
|
||||
keycloak.tokenManager().getAccessTokenString();
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
log.warn("Keycloak non connecté: {}", e.getMessage());
|
||||
@@ -144,37 +120,114 @@ public class KeycloakAdminClientImpl implements KeycloakAdminClient {
|
||||
@Override
|
||||
public boolean realmExists(String realmName) {
|
||||
try {
|
||||
// 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) {
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
@Override
|
||||
public void close() {
|
||||
if (keycloak != null) {
|
||||
log.info("Fermeture de la connexion Keycloak...");
|
||||
keycloak.close();
|
||||
keycloak = null;
|
||||
@Retry(maxRetries = 3, delay = 2, delayUnit = ChronoUnit.SECONDS)
|
||||
@Timeout(value = 30, unit = ChronoUnit.SECONDS)
|
||||
@CircuitBreaker(requestVolumeThreshold = 10, failureRatio = 0.5, delay = 5000)
|
||||
public List<String> getAllRealms() {
|
||||
try {
|
||||
log.debug("Récupération de tous les realms depuis Keycloak");
|
||||
// Appel HTTP direct pour éviter l'erreur de désérialisation de RealmRepresentation
|
||||
// (champ bruteForceStrategy inconnu dans la version de la librairie cliente)
|
||||
String token = keycloak.tokenManager().getAccessTokenString();
|
||||
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(URI.create(serverUrl + "/admin/realms"))
|
||||
.header("Authorization", "Bearer " + token)
|
||||
.header("Accept", "application/json")
|
||||
.GET()
|
||||
.build();
|
||||
|
||||
HttpResponse<String> response = HttpClient.newHttpClient()
|
||||
.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
|
||||
if (response.statusCode() != 200) {
|
||||
throw new RuntimeException("Keycloak returned HTTP " + response.statusCode());
|
||||
}
|
||||
|
||||
ObjectMapper mapper = new ObjectMapper()
|
||||
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||
|
||||
List<Map<String, Object>> realmMaps = mapper.readValue(
|
||||
response.body(), new TypeReference<>() {});
|
||||
|
||||
List<String> realms = realmMaps.stream()
|
||||
.map(r -> (String) r.get("realm"))
|
||||
.filter(r -> r != null)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
log.debug("Realms récupérés: {}", realms);
|
||||
return realms;
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors de la récupération de tous les realms: {}", e.getMessage());
|
||||
throw new RuntimeException("Impossible de récupérer la liste des realms", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Retry(maxRetries = 3, delay = 2, delayUnit = ChronoUnit.SECONDS)
|
||||
@Timeout(value = 30, unit = ChronoUnit.SECONDS)
|
||||
@CircuitBreaker(requestVolumeThreshold = 10, failureRatio = 0.5, delay = 5000)
|
||||
public List<String> getRealmClients(String realmName) {
|
||||
try {
|
||||
log.debug("Récupération des clients du realm {}", realmName);
|
||||
String token = keycloak.tokenManager().getAccessTokenString();
|
||||
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(URI.create(serverUrl + "/admin/realms/" + realmName + "/clients"))
|
||||
.header("Authorization", "Bearer " + token)
|
||||
.header("Accept", "application/json")
|
||||
.GET()
|
||||
.build();
|
||||
|
||||
HttpResponse<String> response = HttpClient.newHttpClient()
|
||||
.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
|
||||
if (response.statusCode() != 200) {
|
||||
throw new RuntimeException("Keycloak returned HTTP " + response.statusCode());
|
||||
}
|
||||
|
||||
ObjectMapper mapper = new ObjectMapper()
|
||||
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||
|
||||
List<Map<String, Object>> clientMaps = mapper.readValue(
|
||||
response.body(), new TypeReference<>() {});
|
||||
|
||||
List<String> clients = clientMaps.stream()
|
||||
.map(c -> (String) c.get("clientId"))
|
||||
.filter(c -> c != null)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
log.debug("Clients récupérés pour {}: {}", realmName, clients);
|
||||
return clients;
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors de la récupération des clients du realm {}: {}", realmName, e.getMessage());
|
||||
throw new RuntimeException("Impossible de récupérer les clients du realm: " + realmName, e);
|
||||
}
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
@Override
|
||||
public void close() {
|
||||
log.info("Fermeture de la connexion Keycloak...");
|
||||
// Le cycle de vie est géré par Quarkus CDI
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reconnect() {
|
||||
log.info("Reconnexion à Keycloak...");
|
||||
close();
|
||||
init();
|
||||
log.info("Reconnexion à Keycloak... (géré par Quarkus CDI)");
|
||||
// Le bean Keycloak est géré par Quarkus, pas de reconnexion manuelle nécessaire
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,17 +4,19 @@ import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.quarkus.jackson.ObjectMapperCustomizer;
|
||||
import jakarta.inject.Singleton;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* Configuration Jackson pour ignorer les propriétés inconnues
|
||||
* Nécessaire pour la compatibilité avec les versions récentes de Keycloak
|
||||
* Configure Jackson globally to ignore unknown JSON properties.
|
||||
* This is required for forward compatibility with newer Keycloak versions (e.g. cpuInfo field).
|
||||
*/
|
||||
@Singleton
|
||||
@Slf4j
|
||||
public class JacksonConfig implements ObjectMapperCustomizer {
|
||||
|
||||
@Override
|
||||
public void customize(ObjectMapper objectMapper) {
|
||||
// Ignorer les propriétés inconnues pour compatibilité Keycloak
|
||||
log.info("### LIONS: Applying Jackson configuration for Keycloak compatibility ###");
|
||||
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package dev.lions.user.manager.config;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.quarkus.jackson.ObjectMapperCustomizer;
|
||||
import jakarta.inject.Singleton;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.keycloak.representations.idm.RoleRepresentation;
|
||||
|
||||
/**
|
||||
* Customizer pour Jackson afin d'ignorer les propriétés inconnues dans les
|
||||
* représentations Keycloak.
|
||||
* Cela évite les erreurs de désérialisation (comme bruteForceStrategy) lorsque
|
||||
* le serveur Keycloak
|
||||
* est plus récent que les bibliothèques clients.
|
||||
*/
|
||||
@Singleton
|
||||
public class KeycloakJacksonCustomizer implements ObjectMapperCustomizer {
|
||||
|
||||
@Override
|
||||
public void customize(ObjectMapper objectMapper) {
|
||||
// En plus de la configuration globale, on force les Mix-ins pour les classes
|
||||
// Keycloak critiques
|
||||
objectMapper.addMixIn(RealmRepresentation.class, IgnoreUnknownMixin.class);
|
||||
objectMapper.addMixIn(UserRepresentation.class, IgnoreUnknownMixin.class);
|
||||
objectMapper.addMixIn(RoleRepresentation.class, IgnoreUnknownMixin.class);
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
abstract static class IgnoreUnknownMixin {
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package dev.lions.user.manager.config;
|
||||
|
||||
import io.quarkus.arc.profile.IfBuildProfile;
|
||||
import io.quarkus.runtime.StartupEvent;
|
||||
import jakarta.enterprise.event.Observes;
|
||||
import jakarta.inject.Inject;
|
||||
@@ -21,6 +22,7 @@ import java.util.*;
|
||||
* S'exécute au démarrage de l'application en mode dev
|
||||
*/
|
||||
@Singleton
|
||||
@IfBuildProfile("dev")
|
||||
@Slf4j
|
||||
public class KeycloakTestUserConfig {
|
||||
|
||||
|
||||
@@ -1,364 +1,171 @@
|
||||
package dev.lions.user.manager.resource;
|
||||
|
||||
import dev.lions.user.manager.api.AuditResourceApi;
|
||||
import dev.lions.user.manager.dto.audit.AuditLogDTO;
|
||||
import dev.lions.user.manager.dto.common.CountDTO;
|
||||
import dev.lions.user.manager.enums.audit.TypeActionAudit;
|
||||
import dev.lions.user.manager.service.AuditService;
|
||||
import jakarta.annotation.security.RolesAllowed;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.ws.rs.*;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||
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.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* REST Resource pour l'audit et la consultation des logs
|
||||
* Implémente l'interface API commune.
|
||||
*/
|
||||
@Path("/api/audit")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Tag(name = "Audit", description = "Consultation des logs d'audit et statistiques")
|
||||
@Slf4j
|
||||
public class AuditResource {
|
||||
@jakarta.enterprise.context.ApplicationScoped
|
||||
@jakarta.ws.rs.Path("/api/audit")
|
||||
public class AuditResource implements AuditResourceApi {
|
||||
|
||||
private static final String DEFAULT_REALM_VALUE = "master";
|
||||
|
||||
@Inject
|
||||
AuditService auditService;
|
||||
|
||||
@POST
|
||||
@Path("/search")
|
||||
@Operation(summary = "Rechercher des logs d'audit", description = "Recherche avancée de logs selon critères")
|
||||
@APIResponses({
|
||||
@APIResponse(responseCode = "200", description = "Résultats de recherche"),
|
||||
@APIResponse(responseCode = "500", description = "Erreur serveur")
|
||||
})
|
||||
@RolesAllowed({"admin", "auditor"})
|
||||
public Response searchLogs(
|
||||
@QueryParam("acteur") String acteurUsername,
|
||||
@QueryParam("dateDebut") String dateDebutStr,
|
||||
@QueryParam("dateFin") String dateFinStr,
|
||||
@QueryParam("typeAction") TypeActionAudit typeAction,
|
||||
@QueryParam("ressourceType") String ressourceType,
|
||||
@QueryParam("succes") Boolean succes,
|
||||
@QueryParam("page") @DefaultValue("0") int page,
|
||||
@QueryParam("pageSize") @DefaultValue("50") int pageSize
|
||||
) {
|
||||
@ConfigProperty(name = "lions.keycloak.admin-realm", defaultValue = DEFAULT_REALM_VALUE)
|
||||
String defaultRealm;
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "auditor" })
|
||||
public List<AuditLogDTO> searchLogs(
|
||||
String acteurUsername,
|
||||
String dateDebutStr,
|
||||
String dateFinStr,
|
||||
TypeActionAudit typeAction,
|
||||
String ressourceType,
|
||||
Boolean succes,
|
||||
int page,
|
||||
int pageSize) {
|
||||
log.info("POST /api/audit/search - Recherche de logs");
|
||||
|
||||
try {
|
||||
LocalDateTime dateDebut = dateDebutStr != null ? LocalDateTime.parse(dateDebutStr) : null;
|
||||
LocalDateTime dateFin = dateFinStr != null ? LocalDateTime.parse(dateFinStr) : null;
|
||||
LocalDateTime dateDebut = dateDebutStr != null ? LocalDateTime.parse(dateDebutStr) : null;
|
||||
LocalDateTime dateFin = dateFinStr != null ? LocalDateTime.parse(dateFinStr) : null;
|
||||
|
||||
// Utiliser findByActeur si acteurUsername est fourni, sinon findByRealm
|
||||
List<AuditLogDTO> logs;
|
||||
if (acteurUsername != null && !acteurUsername.isBlank()) {
|
||||
logs = auditService.findByActeur(acteurUsername, dateDebut, dateFin, page, pageSize);
|
||||
} else {
|
||||
// Pour une recherche générale, utiliser findByRealm (on utilise "master" par défaut)
|
||||
logs = auditService.findByRealm("master", dateDebut, dateFin, page, pageSize);
|
||||
}
|
||||
|
||||
// Filtrer par typeAction, ressourceType et succes si fournis
|
||||
if (typeAction != null || ressourceType != null || succes != null) {
|
||||
logs = logs.stream()
|
||||
// Utiliser findByActeur si acteurUsername est fourni, sinon findByRealm
|
||||
List<AuditLogDTO> logs;
|
||||
if (acteurUsername != null && !acteurUsername.isBlank()) {
|
||||
logs = auditService.findByActeur(acteurUsername, dateDebut, dateFin, page, pageSize);
|
||||
} else {
|
||||
// Pour une recherche générale, utiliser findByRealm (on utilise defaultRealm par
|
||||
// défaut)
|
||||
logs = auditService.findByRealm(defaultRealm, dateDebut, dateFin, page, pageSize);
|
||||
}
|
||||
|
||||
// Filtrer par typeAction, ressourceType et succes si fournis
|
||||
if (typeAction != null || ressourceType != null || succes != null) {
|
||||
logs = logs.stream()
|
||||
.filter(log -> typeAction == null || typeAction.equals(log.getTypeAction()))
|
||||
.filter(log -> ressourceType == null || ressourceType.equals(log.getRessourceType()))
|
||||
.filter(log -> succes == null || succes == log.isSuccessful())
|
||||
.collect(java.util.stream.Collectors.toList());
|
||||
}
|
||||
|
||||
return Response.ok(logs).build();
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors de la recherche de logs d'audit", e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity(new ErrorResponse(e.getMessage()))
|
||||
.build();
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
return logs;
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/actor/{acteurUsername}")
|
||||
@Operation(summary = "Récupérer les logs d'un acteur", description = "Liste les derniers logs d'un utilisateur")
|
||||
@APIResponses({
|
||||
@APIResponse(responseCode = "200", description = "Liste des logs"),
|
||||
@APIResponse(responseCode = "500", description = "Erreur serveur")
|
||||
})
|
||||
@RolesAllowed({"admin", "auditor"})
|
||||
public Response getLogsByActor(
|
||||
@Parameter(description = "Username de l'acteur") @PathParam("acteurUsername") @NotBlank String acteurUsername,
|
||||
@Parameter(description = "Nombre de logs à retourner") @QueryParam("limit") @DefaultValue("100") int limit
|
||||
) {
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "auditor" })
|
||||
public List<AuditLogDTO> getLogsByActor(String acteurUsername, int limit) {
|
||||
log.info("GET /api/audit/actor/{} - Limite: {}", acteurUsername, limit);
|
||||
|
||||
try {
|
||||
List<AuditLogDTO> logs = auditService.findByActeur(acteurUsername, null, null, 0, limit);
|
||||
return Response.ok(logs).build();
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors de la récupération des logs de l'acteur {}", acteurUsername, e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity(new ErrorResponse(e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
return auditService.findByActeur(acteurUsername, null, null, 0, limit);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/resource/{ressourceType}/{ressourceId}")
|
||||
@Operation(summary = "Récupérer les logs d'une ressource", description = "Liste les derniers logs d'une ressource spécifique")
|
||||
@APIResponses({
|
||||
@APIResponse(responseCode = "200", description = "Liste des logs"),
|
||||
@APIResponse(responseCode = "500", description = "Erreur serveur")
|
||||
})
|
||||
@RolesAllowed({"admin", "auditor"})
|
||||
public Response getLogsByResource(
|
||||
@PathParam("ressourceType") @NotBlank String ressourceType,
|
||||
@PathParam("ressourceId") @NotBlank String ressourceId,
|
||||
@QueryParam("limit") @DefaultValue("100") int limit
|
||||
) {
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "auditor" })
|
||||
public List<AuditLogDTO> getLogsByResource(String ressourceType, String ressourceId, int limit) {
|
||||
log.info("GET /api/audit/resource/{}/{} - Limite: {}", ressourceType, ressourceId, limit);
|
||||
|
||||
try {
|
||||
List<AuditLogDTO> logs = auditService.findByRessource(ressourceType, ressourceId, null, null, 0, limit);
|
||||
return Response.ok(logs).build();
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors de la récupération des logs de la ressource {}:{}",
|
||||
ressourceType, ressourceId, e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity(new ErrorResponse(e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
return auditService.findByRessource(ressourceType, ressourceId, null, null, 0, limit);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/action/{typeAction}")
|
||||
@Operation(summary = "Récupérer les logs par type d'action", description = "Liste les logs d'un type d'action spécifique")
|
||||
@APIResponses({
|
||||
@APIResponse(responseCode = "200", description = "Liste des logs"),
|
||||
@APIResponse(responseCode = "500", description = "Erreur serveur")
|
||||
})
|
||||
@RolesAllowed({"admin", "auditor"})
|
||||
public Response getLogsByAction(
|
||||
@PathParam("typeAction") TypeActionAudit typeAction,
|
||||
@QueryParam("dateDebut") String dateDebutStr,
|
||||
@QueryParam("dateFin") String dateFinStr,
|
||||
@QueryParam("limit") @DefaultValue("100") int limit
|
||||
) {
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "auditor" })
|
||||
public List<AuditLogDTO> getLogsByAction(TypeActionAudit typeAction, String dateDebutStr, String dateFinStr,
|
||||
int limit) {
|
||||
log.info("GET /api/audit/action/{} - Limite: {}", typeAction, limit);
|
||||
LocalDateTime dateDebut = dateDebutStr != null ? LocalDateTime.parse(dateDebutStr) : null;
|
||||
LocalDateTime dateFin = dateFinStr != null ? LocalDateTime.parse(dateFinStr) : null;
|
||||
|
||||
try {
|
||||
LocalDateTime dateDebut = dateDebutStr != null ? LocalDateTime.parse(dateDebutStr) : null;
|
||||
LocalDateTime dateFin = dateFinStr != null ? LocalDateTime.parse(dateFinStr) : null;
|
||||
|
||||
List<AuditLogDTO> logs = auditService.findByTypeAction(typeAction, "master", dateDebut, dateFin, 0, limit);
|
||||
return Response.ok(logs).build();
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors de la récupération des logs de type {}", typeAction, e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity(new ErrorResponse(e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
return auditService.findByTypeAction(typeAction, defaultRealm, dateDebut, dateFin, 0, limit);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/stats/actions")
|
||||
@Operation(summary = "Statistiques par type d'action", description = "Retourne le nombre de logs par type d'action")
|
||||
@APIResponses({
|
||||
@APIResponse(responseCode = "200", description = "Statistiques des actions"),
|
||||
@APIResponse(responseCode = "500", description = "Erreur serveur")
|
||||
})
|
||||
@RolesAllowed({"admin", "auditor"})
|
||||
public Response getActionStatistics(
|
||||
@QueryParam("dateDebut") String dateDebutStr,
|
||||
@QueryParam("dateFin") String dateFinStr
|
||||
) {
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "auditor" })
|
||||
public Map<TypeActionAudit, Long> getActionStatistics(String dateDebutStr, String dateFinStr) {
|
||||
log.info("GET /api/audit/stats/actions - Période: {} à {}", dateDebutStr, dateFinStr);
|
||||
LocalDateTime dateDebut = dateDebutStr != null ? LocalDateTime.parse(dateDebutStr) : null;
|
||||
LocalDateTime dateFin = dateFinStr != null ? LocalDateTime.parse(dateFinStr) : null;
|
||||
|
||||
try {
|
||||
LocalDateTime dateDebut = dateDebutStr != null ? LocalDateTime.parse(dateDebutStr) : null;
|
||||
LocalDateTime dateFin = dateFinStr != null ? LocalDateTime.parse(dateFinStr) : null;
|
||||
|
||||
Map<TypeActionAudit, Long> stats = auditService.countByActionType("master", dateDebut, dateFin);
|
||||
return Response.ok(stats).build();
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors du calcul des statistiques d'actions", e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity(new ErrorResponse(e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
return auditService.countByActionType(defaultRealm, dateDebut, dateFin);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/stats/users")
|
||||
@Operation(summary = "Statistiques par utilisateur", description = "Retourne le nombre d'actions par utilisateur")
|
||||
@APIResponses({
|
||||
@APIResponse(responseCode = "200", description = "Statistiques des utilisateurs"),
|
||||
@APIResponse(responseCode = "500", description = "Erreur serveur")
|
||||
})
|
||||
@RolesAllowed({"admin", "auditor"})
|
||||
public Response getUserActivityStatistics(
|
||||
@QueryParam("dateDebut") String dateDebutStr,
|
||||
@QueryParam("dateFin") String dateFinStr
|
||||
) {
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "auditor" })
|
||||
public Map<String, Long> getUserActivityStatistics(String dateDebutStr, String dateFinStr) {
|
||||
log.info("GET /api/audit/stats/users - Période: {} à {}", dateDebutStr, dateFinStr);
|
||||
LocalDateTime dateDebut = dateDebutStr != null ? LocalDateTime.parse(dateDebutStr) : null;
|
||||
LocalDateTime dateFin = dateFinStr != null ? LocalDateTime.parse(dateFinStr) : null;
|
||||
|
||||
try {
|
||||
LocalDateTime dateDebut = dateDebutStr != null ? LocalDateTime.parse(dateDebutStr) : null;
|
||||
LocalDateTime dateFin = dateFinStr != null ? LocalDateTime.parse(dateFinStr) : null;
|
||||
|
||||
Map<String, Long> stats = auditService.countByActeur("master", dateDebut, dateFin);
|
||||
return Response.ok(stats).build();
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors du calcul des statistiques utilisateurs", e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity(new ErrorResponse(e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
return auditService.countByActeur(defaultRealm, dateDebut, dateFin);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/stats/failures")
|
||||
@Operation(summary = "Comptage des échecs", description = "Retourne le nombre d'échecs sur une période")
|
||||
@APIResponses({
|
||||
@APIResponse(responseCode = "200", description = "Nombre d'échecs"),
|
||||
@APIResponse(responseCode = "500", description = "Erreur serveur")
|
||||
})
|
||||
@RolesAllowed({"admin", "auditor"})
|
||||
public Response getFailureCount(
|
||||
@QueryParam("dateDebut") String dateDebutStr,
|
||||
@QueryParam("dateFin") String dateFinStr
|
||||
) {
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "auditor" })
|
||||
public CountDTO getFailureCount(String dateDebutStr, String dateFinStr) {
|
||||
log.info("GET /api/audit/stats/failures - Période: {} à {}", dateDebutStr, dateFinStr);
|
||||
LocalDateTime dateDebut = dateDebutStr != null ? LocalDateTime.parse(dateDebutStr) : null;
|
||||
LocalDateTime dateFin = dateFinStr != null ? LocalDateTime.parse(dateFinStr) : null;
|
||||
|
||||
try {
|
||||
LocalDateTime dateDebut = dateDebutStr != null ? LocalDateTime.parse(dateDebutStr) : null;
|
||||
LocalDateTime dateFin = dateFinStr != null ? LocalDateTime.parse(dateFinStr) : null;
|
||||
|
||||
Map<String, Long> successVsFailure = auditService.countSuccessVsFailure("master", dateDebut, dateFin);
|
||||
long count = successVsFailure.getOrDefault("failure", 0L);
|
||||
return Response.ok(new CountResponse(count)).build();
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors du comptage des échecs", e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity(new ErrorResponse(e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
Map<String, Long> successVsFailure = auditService.countSuccessVsFailure(defaultRealm, dateDebut, dateFin);
|
||||
long count = successVsFailure.getOrDefault("failure", 0L);
|
||||
return new CountDTO(count);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/stats/success")
|
||||
@Operation(summary = "Comptage des succès", description = "Retourne le nombre de succès sur une période")
|
||||
@APIResponses({
|
||||
@APIResponse(responseCode = "200", description = "Nombre de succès"),
|
||||
@APIResponse(responseCode = "500", description = "Erreur serveur")
|
||||
})
|
||||
@RolesAllowed({"admin", "auditor"})
|
||||
public Response getSuccessCount(
|
||||
@QueryParam("dateDebut") String dateDebutStr,
|
||||
@QueryParam("dateFin") String dateFinStr
|
||||
) {
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "auditor" })
|
||||
public CountDTO getSuccessCount(String dateDebutStr, String dateFinStr) {
|
||||
log.info("GET /api/audit/stats/success - Période: {} à {}", dateDebutStr, dateFinStr);
|
||||
LocalDateTime dateDebut = dateDebutStr != null ? LocalDateTime.parse(dateDebutStr) : null;
|
||||
LocalDateTime dateFin = dateFinStr != null ? LocalDateTime.parse(dateFinStr) : null;
|
||||
|
||||
try {
|
||||
LocalDateTime dateDebut = dateDebutStr != null ? LocalDateTime.parse(dateDebutStr) : null;
|
||||
LocalDateTime dateFin = dateFinStr != null ? LocalDateTime.parse(dateFinStr) : null;
|
||||
|
||||
Map<String, Long> successVsFailure = auditService.countSuccessVsFailure("master", dateDebut, dateFin);
|
||||
long count = successVsFailure.getOrDefault("success", 0L);
|
||||
return Response.ok(new CountResponse(count)).build();
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors du comptage des succès", e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity(new ErrorResponse(e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
Map<String, Long> successVsFailure = auditService.countSuccessVsFailure(defaultRealm, dateDebut, dateFin);
|
||||
long count = successVsFailure.getOrDefault("success", 0L);
|
||||
return new CountDTO(count);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/export/csv")
|
||||
@Produces(MediaType.TEXT_PLAIN)
|
||||
@Operation(summary = "Exporter les logs en CSV", description = "Génère un fichier CSV des logs d'audit")
|
||||
@APIResponses({
|
||||
@APIResponse(responseCode = "200", description = "Fichier CSV généré"),
|
||||
@APIResponse(responseCode = "500", description = "Erreur serveur")
|
||||
})
|
||||
@RolesAllowed({"admin", "auditor"})
|
||||
public Response exportLogsToCSV(
|
||||
@QueryParam("dateDebut") String dateDebutStr,
|
||||
@QueryParam("dateFin") String dateFinStr
|
||||
) {
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "auditor" })
|
||||
public Response exportLogsToCSV(String dateDebutStr, String dateFinStr) {
|
||||
log.info("GET /api/audit/export/csv - Période: {} à {}", dateDebutStr, dateFinStr);
|
||||
|
||||
try {
|
||||
LocalDateTime dateDebut = dateDebutStr != null ? LocalDateTime.parse(dateDebutStr) : null;
|
||||
LocalDateTime dateFin = dateFinStr != null ? LocalDateTime.parse(dateFinStr) : null;
|
||||
|
||||
String csvContent = auditService.exportToCSV("master", dateDebut, dateFin);
|
||||
String csvContent = auditService.exportToCSV(defaultRealm, dateDebut, dateFin);
|
||||
|
||||
return Response.ok(csvContent)
|
||||
.header("Content-Disposition", "attachment; filename=\"audit-logs-" +
|
||||
LocalDateTime.now().toString().replace(":", "-") + ".csv\"")
|
||||
.build();
|
||||
.header("Content-Disposition", "attachment; filename=\"audit-logs-" +
|
||||
LocalDateTime.now().toString().replace(":", "-") + ".csv\"")
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors de l'export CSV des logs", e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity(new ErrorResponse(e.getMessage()))
|
||||
.build();
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@DELETE
|
||||
@Path("/purge")
|
||||
@Operation(summary = "Purger les anciens logs", description = "Supprime les logs de plus de X jours")
|
||||
@APIResponses({
|
||||
@APIResponse(responseCode = "204", description = "Purge effectuée"),
|
||||
@APIResponse(responseCode = "500", description = "Erreur serveur")
|
||||
})
|
||||
@RolesAllowed({"admin"})
|
||||
public Response purgeOldLogs(
|
||||
@QueryParam("joursAnciennete") @DefaultValue("90") int joursAnciennete
|
||||
) {
|
||||
@Override
|
||||
@RolesAllowed({ "admin" })
|
||||
public void purgeOldLogs(int joursAnciennete) {
|
||||
log.info("DELETE /api/audit/purge - Suppression des logs de plus de {} jours", joursAnciennete);
|
||||
|
||||
try {
|
||||
LocalDateTime dateLimite = LocalDateTime.now().minusDays(joursAnciennete);
|
||||
auditService.purgeOldLogs(dateLimite);
|
||||
return Response.noContent().build();
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors de la purge des logs", e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity(new ErrorResponse(e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== DTOs internes ====================
|
||||
|
||||
@Schema(description = "Réponse de comptage")
|
||||
public static class CountResponse {
|
||||
@Schema(description = "Nombre d'éléments")
|
||||
public long count;
|
||||
|
||||
public CountResponse(long count) {
|
||||
this.count = count;
|
||||
}
|
||||
}
|
||||
|
||||
@Schema(description = "Réponse d'erreur")
|
||||
public static class ErrorResponse {
|
||||
@Schema(description = "Message d'erreur")
|
||||
public String message;
|
||||
|
||||
public ErrorResponse(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
LocalDateTime dateLimite = LocalDateTime.now().minusDays(joursAnciennete);
|
||||
auditService.purgeOldLogs(dateLimite);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
package dev.lions.user.manager.resource;
|
||||
|
||||
import dev.lions.user.manager.api.RealmAssignmentResourceApi;
|
||||
import dev.lions.user.manager.dto.realm.AuthorizedRealmsDTO;
|
||||
import dev.lions.user.manager.dto.realm.RealmAccessCheckDTO;
|
||||
import dev.lions.user.manager.dto.realm.RealmAssignmentDTO;
|
||||
import dev.lions.user.manager.service.RealmAuthorizationService;
|
||||
import jakarta.annotation.security.RolesAllowed;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.ws.rs.core.Context;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import jakarta.ws.rs.core.SecurityContext;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* REST Resource pour la gestion des affectations de realms aux utilisateurs
|
||||
* Implémente l'interface API commune.
|
||||
*/
|
||||
@Slf4j
|
||||
@jakarta.enterprise.context.ApplicationScoped
|
||||
@jakarta.ws.rs.Path("/api/realm-assignments")
|
||||
public class RealmAssignmentResource implements RealmAssignmentResourceApi {
|
||||
|
||||
@Inject
|
||||
RealmAuthorizationService realmAuthorizationService;
|
||||
|
||||
@Context
|
||||
SecurityContext securityContext;
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin" })
|
||||
public List<RealmAssignmentDTO> getAllAssignments() {
|
||||
log.info("GET /api/realm-assignments - Récupération de toutes les affectations");
|
||||
return realmAuthorizationService.getAllAssignments();
|
||||
}
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "user_manager" })
|
||||
public List<RealmAssignmentDTO> getAssignmentsByUser(String userId) {
|
||||
log.info("GET /api/realm-assignments/user/{}", userId);
|
||||
return realmAuthorizationService.getAssignmentsByUser(userId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin" })
|
||||
public List<RealmAssignmentDTO> getAssignmentsByRealm(String realmName) {
|
||||
log.info("GET /api/realm-assignments/realm/{}", realmName);
|
||||
return realmAuthorizationService.getAssignmentsByRealm(realmName);
|
||||
}
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin" })
|
||||
public RealmAssignmentDTO getAssignmentById(String assignmentId) {
|
||||
log.info("GET /api/realm-assignments/{}", assignmentId);
|
||||
return realmAuthorizationService.getAssignmentById(assignmentId)
|
||||
.orElseThrow(() -> new RuntimeException("Affectation non trouvée")); // ExceptionMapper should
|
||||
// handle/map to 404
|
||||
}
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "user_manager" })
|
||||
public RealmAccessCheckDTO canManageRealm(String userId, String realmName) {
|
||||
log.info("GET /api/realm-assignments/check - userId: {}, realmName: {}", userId, realmName);
|
||||
boolean canManage = realmAuthorizationService.canManageRealm(userId, realmName);
|
||||
return new RealmAccessCheckDTO(canManage, userId, realmName);
|
||||
}
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "user_manager" })
|
||||
public AuthorizedRealmsDTO getAuthorizedRealms(String userId) {
|
||||
log.info("GET /api/realm-assignments/authorized-realms/{}", userId);
|
||||
List<String> realms = realmAuthorizationService.getAuthorizedRealms(userId);
|
||||
boolean isSuperAdmin = realmAuthorizationService.isSuperAdmin(userId);
|
||||
return new AuthorizedRealmsDTO(realms, isSuperAdmin);
|
||||
}
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin" })
|
||||
public Response assignRealmToUser(@Valid @NotNull RealmAssignmentDTO assignment) {
|
||||
log.info("POST /api/realm-assignments - Assignation du realm {} à l'utilisateur {}",
|
||||
assignment.getRealmName(), assignment.getUserId());
|
||||
|
||||
try {
|
||||
// Ajouter l'utilisateur qui fait l'assignation
|
||||
if (securityContext.getUserPrincipal() != null) {
|
||||
assignment.setAssignedBy(securityContext.getUserPrincipal().getName());
|
||||
}
|
||||
|
||||
RealmAssignmentDTO createdAssignment = realmAuthorizationService.assignRealmToUser(assignment);
|
||||
return Response.status(Response.Status.CREATED).entity(createdAssignment).build();
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.warn("Données invalides lors de l'assignation: {}", e.getMessage());
|
||||
// Need to return 409 or 400 manually since this method returns Response
|
||||
return Response.status(Response.Status.CONFLICT)
|
||||
.entity(new dev.lions.user.manager.dto.common.ApiErrorDTO(e.getMessage()))
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors de l'assignation du realm", e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin" })
|
||||
public void revokeRealmFromUser(String userId, String realmName) {
|
||||
log.info("DELETE /api/realm-assignments/user/{}/realm/{}", userId, realmName);
|
||||
realmAuthorizationService.revokeRealmFromUser(userId, realmName);
|
||||
}
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin" })
|
||||
public void revokeAllRealmsFromUser(String userId) {
|
||||
log.info("DELETE /api/realm-assignments/user/{}", userId);
|
||||
realmAuthorizationService.revokeAllRealmsFromUser(userId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin" })
|
||||
public void deactivateAssignment(String assignmentId) {
|
||||
log.info("PUT /api/realm-assignments/{}/deactivate", assignmentId);
|
||||
realmAuthorizationService.deactivateAssignment(assignmentId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin" })
|
||||
public void activateAssignment(String assignmentId) {
|
||||
log.info("PUT /api/realm-assignments/{}/activate", assignmentId);
|
||||
realmAuthorizationService.activateAssignment(assignmentId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin" })
|
||||
public void setSuperAdmin(String userId, @NotNull Boolean superAdmin) {
|
||||
log.info("PUT /api/realm-assignments/super-admin/{} - superAdmin: {}", userId, superAdmin);
|
||||
realmAuthorizationService.setSuperAdmin(userId, superAdmin);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package dev.lions.user.manager.resource;
|
||||
|
||||
import dev.lions.user.manager.api.RealmResourceApi;
|
||||
import dev.lions.user.manager.client.KeycloakAdminClient;
|
||||
import io.quarkus.security.identity.SecurityIdentity;
|
||||
import jakarta.annotation.security.RolesAllowed;
|
||||
import jakarta.inject.Inject;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Ressource REST pour la gestion des realms Keycloak
|
||||
* Implémente l'interface API commune.
|
||||
*/
|
||||
@Slf4j
|
||||
@jakarta.enterprise.context.ApplicationScoped
|
||||
@jakarta.ws.rs.Path("/api/realms")
|
||||
public class RealmResource implements RealmResourceApi {
|
||||
|
||||
@Inject
|
||||
KeycloakAdminClient keycloakAdminClient;
|
||||
|
||||
@Inject
|
||||
SecurityIdentity securityIdentity;
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "user_manager", "user_viewer", "role_manager", "role_viewer" })
|
||||
public List<String> getAllRealms() {
|
||||
log.info("GET /api/realms/list");
|
||||
|
||||
try {
|
||||
List<String> realms = keycloakAdminClient.getAllRealms();
|
||||
log.info("Récupération réussie: {} realms trouvés", realms.size());
|
||||
return realms;
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors de la récupération des realms", e);
|
||||
throw new RuntimeException("Erreur lors de la récupération des realms: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "user_manager", "role_manager", "role_viewer" })
|
||||
public List<String> getRealmClients(String realmName) {
|
||||
log.info("GET /api/realms/{}/clients", realmName);
|
||||
|
||||
try {
|
||||
List<String> clients = keycloakAdminClient.getRealmClients(realmName);
|
||||
log.info("Récupération réussie: {} clients trouvés pour le realm {}", clients.size(), realmName);
|
||||
return clients;
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors de la récupération des clients du realm {}", realmName, e);
|
||||
throw new RuntimeException("Erreur lors de la récupération des clients: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,62 +1,50 @@
|
||||
package dev.lions.user.manager.resource;
|
||||
|
||||
import dev.lions.user.manager.api.RoleResourceApi;
|
||||
import dev.lions.user.manager.dto.common.ApiErrorDTO;
|
||||
import dev.lions.user.manager.dto.role.RoleAssignmentDTO;
|
||||
import dev.lions.user.manager.dto.role.RoleAssignmentRequestDTO;
|
||||
import dev.lions.user.manager.dto.role.RoleDTO;
|
||||
import dev.lions.user.manager.enums.role.TypeRole;
|
||||
import dev.lions.user.manager.service.RoleService;
|
||||
import jakarta.annotation.security.RolesAllowed;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.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;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* REST Resource pour la gestion des rôles Keycloak
|
||||
* Endpoints pour les rôles realm, rôles client, et attributions
|
||||
* Implémente l'interface API commune.
|
||||
* Annotation explicite des méthodes pour éviter les problèmes d'héritage JAX-RS
|
||||
* dans Quarkus.
|
||||
*/
|
||||
@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 {
|
||||
@jakarta.enterprise.context.ApplicationScoped
|
||||
@Path("/api/roles")
|
||||
public class RoleResource implements RoleResourceApi {
|
||||
|
||||
@Inject
|
||||
RoleService roleService;
|
||||
|
||||
// ==================== Endpoints Realm Roles ====================
|
||||
|
||||
@Override
|
||||
@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"})
|
||||
@RolesAllowed({ "admin", "role_manager" })
|
||||
public Response createRealmRole(
|
||||
@Valid @NotNull RoleDTO roleDTO,
|
||||
@QueryParam("realm") @NotBlank String realmName
|
||||
) {
|
||||
@Valid @NotNull RoleDTO roleDTO,
|
||||
@QueryParam("realm") String realmName) {
|
||||
log.info("POST /api/roles/realm - Création du rôle realm: {} dans le realm: {}",
|
||||
roleDTO.getName(), realmName);
|
||||
roleDTO.getName(), realmName);
|
||||
|
||||
try {
|
||||
RoleDTO createdRole = roleService.createRealmRole(roleDTO, realmName);
|
||||
@@ -64,164 +52,74 @@ public class RoleResource {
|
||||
} 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();
|
||||
.entity(new ApiErrorDTO(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();
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@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
|
||||
) {
|
||||
@RolesAllowed({ "admin", "role_manager", "role_viewer" })
|
||||
public RoleDTO getRealmRole(@PathParam("roleName") String roleName, @QueryParam("realm") String realmName) {
|
||||
log.info("GET /api/roles/realm/{} - realm: {}", roleName, realmName);
|
||||
|
||||
try {
|
||||
return roleService.getRoleByName(roleName, realmName, dev.lions.user.manager.enums.role.TypeRole.REALM_ROLE, null)
|
||||
.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();
|
||||
}
|
||||
return roleService.getRoleByName(roleName, realmName, TypeRole.REALM_ROLE, null)
|
||||
.orElseThrow(() -> new RuntimeException("Rôle non trouvé"));
|
||||
}
|
||||
|
||||
@Override
|
||||
@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
|
||||
) {
|
||||
@RolesAllowed({ "admin", "role_manager", "role_viewer" })
|
||||
public List<RoleDTO> getAllRealmRoles(@QueryParam("realm") String realmName) {
|
||||
log.info("GET /api/roles/realm - realm: {}", realmName);
|
||||
|
||||
try {
|
||||
List<RoleDTO> roles = roleService.getAllRealmRoles(realmName);
|
||||
return Response.ok(roles).build();
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors de la récupération des rôles realm", e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity(new ErrorResponse(e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
return roleService.getAllRealmRoles(realmName);
|
||||
}
|
||||
|
||||
@Override
|
||||
@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
|
||||
) {
|
||||
@RolesAllowed({ "admin", "role_manager" })
|
||||
public RoleDTO updateRealmRole(@PathParam("roleName") String roleName, @Valid @NotNull RoleDTO roleDTO,
|
||||
@QueryParam("realm") String realmName) {
|
||||
log.info("PUT /api/roles/realm/{} - realm: {}", roleName, realmName);
|
||||
|
||||
try {
|
||||
// Récupérer l'ID du rôle par son nom
|
||||
Optional<RoleDTO> existingRole = roleService.getRoleByName(roleName, realmName,
|
||||
dev.lions.user.manager.enums.role.TypeRole.REALM_ROLE, null);
|
||||
if (existingRole.isEmpty()) {
|
||||
return Response.status(Response.Status.NOT_FOUND)
|
||||
.entity(new ErrorResponse("Rôle non trouvé"))
|
||||
.build();
|
||||
}
|
||||
|
||||
RoleDTO updatedRole = roleService.updateRole(existingRole.get().getId(), roleDTO, realmName,
|
||||
dev.lions.user.manager.enums.role.TypeRole.REALM_ROLE, null);
|
||||
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();
|
||||
Optional<RoleDTO> existingRole = roleService.getRoleByName(roleName, realmName, TypeRole.REALM_ROLE, null);
|
||||
if (existingRole.isEmpty()) {
|
||||
throw new RuntimeException("Rôle non trouvé");
|
||||
}
|
||||
|
||||
return roleService.updateRole(existingRole.get().getId(), roleDTO, realmName, TypeRole.REALM_ROLE, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
@DELETE
|
||||
@Path("/realm/{roleName}")
|
||||
@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
|
||||
) {
|
||||
@RolesAllowed({ "admin" })
|
||||
public void deleteRealmRole(@PathParam("roleName") String roleName, @QueryParam("realm") String realmName) {
|
||||
log.info("DELETE /api/roles/realm/{} - realm: {}", roleName, realmName);
|
||||
|
||||
try {
|
||||
// Récupérer l'ID du rôle par son nom
|
||||
Optional<RoleDTO> existingRole = roleService.getRoleByName(roleName, realmName,
|
||||
dev.lions.user.manager.enums.role.TypeRole.REALM_ROLE, null);
|
||||
if (existingRole.isEmpty()) {
|
||||
return Response.status(Response.Status.NOT_FOUND)
|
||||
.entity(new ErrorResponse("Rôle non trouvé"))
|
||||
.build();
|
||||
}
|
||||
|
||||
roleService.deleteRole(existingRole.get().getId(), realmName,
|
||||
dev.lions.user.manager.enums.role.TypeRole.REALM_ROLE, null);
|
||||
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();
|
||||
Optional<RoleDTO> existingRole = roleService.getRoleByName(roleName, realmName, TypeRole.REALM_ROLE, null);
|
||||
if (existingRole.isEmpty()) {
|
||||
throw new RuntimeException("Rôle non trouvé");
|
||||
}
|
||||
|
||||
roleService.deleteRole(existingRole.get().getId(), realmName, TypeRole.REALM_ROLE, null);
|
||||
}
|
||||
|
||||
// ==================== Endpoints Client Roles ====================
|
||||
|
||||
@Override
|
||||
@POST
|
||||
@Path("/client/{clientId}")
|
||||
@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
|
||||
) {
|
||||
@RolesAllowed({ "admin", "role_manager" })
|
||||
public Response createClientRole(@PathParam("clientId") String clientId, @Valid @NotNull RoleDTO roleDTO,
|
||||
@QueryParam("realm") String realmName) {
|
||||
log.info("POST /api/roles/client/{} - Création du rôle client dans le realm: {}",
|
||||
clientId, realmName);
|
||||
clientId, realmName);
|
||||
|
||||
try {
|
||||
RoleDTO createdRole = roleService.createClientRole(roleDTO, clientId, realmName);
|
||||
@@ -229,359 +127,164 @@ public class RoleResource {
|
||||
} 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();
|
||||
.entity(new ApiErrorDTO(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();
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@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
|
||||
) {
|
||||
@RolesAllowed({ "admin", "role_manager", "role_viewer" })
|
||||
public RoleDTO getClientRole(@PathParam("clientId") String clientId, @PathParam("roleName") String roleName,
|
||||
@QueryParam("realm") String realmName) {
|
||||
log.info("GET /api/roles/client/{}/{} - realm: {}", clientId, roleName, realmName);
|
||||
|
||||
try {
|
||||
return roleService.getRoleByName(roleName, realmName,
|
||||
dev.lions.user.manager.enums.role.TypeRole.CLIENT_ROLE, clientId)
|
||||
.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();
|
||||
}
|
||||
return roleService.getRoleByName(roleName, realmName, TypeRole.CLIENT_ROLE, clientId)
|
||||
.orElseThrow(() -> new RuntimeException("Rôle client non trouvé"));
|
||||
}
|
||||
|
||||
@Override
|
||||
@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
|
||||
) {
|
||||
@RolesAllowed({ "admin", "role_manager", "role_viewer" })
|
||||
public List<RoleDTO> getAllClientRoles(@PathParam("clientId") String clientId,
|
||||
@QueryParam("realm") String realmName) {
|
||||
log.info("GET /api/roles/client/{} - realm: {}", clientId, realmName);
|
||||
|
||||
try {
|
||||
List<RoleDTO> roles = roleService.getAllClientRoles(realmName, clientId);
|
||||
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();
|
||||
}
|
||||
return roleService.getAllClientRoles(realmName, clientId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@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
|
||||
) {
|
||||
@RolesAllowed({ "admin" })
|
||||
public void deleteClientRole(@PathParam("clientId") String clientId, @PathParam("roleName") String roleName,
|
||||
@QueryParam("realm") String realmName) {
|
||||
log.info("DELETE /api/roles/client/{}/{} - realm: {}", clientId, roleName, realmName);
|
||||
|
||||
try {
|
||||
// Récupérer l'ID du rôle par son nom
|
||||
Optional<RoleDTO> existingRole = roleService.getRoleByName(roleName, realmName,
|
||||
dev.lions.user.manager.enums.role.TypeRole.CLIENT_ROLE, clientId);
|
||||
if (existingRole.isEmpty()) {
|
||||
return Response.status(Response.Status.NOT_FOUND)
|
||||
.entity(new ErrorResponse("Rôle client non trouvé"))
|
||||
.build();
|
||||
}
|
||||
|
||||
roleService.deleteRole(existingRole.get().getId(), realmName,
|
||||
dev.lions.user.manager.enums.role.TypeRole.CLIENT_ROLE, clientId);
|
||||
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();
|
||||
Optional<RoleDTO> existingRole = roleService.getRoleByName(roleName, realmName, TypeRole.CLIENT_ROLE, clientId);
|
||||
if (existingRole.isEmpty()) {
|
||||
throw new RuntimeException("Rôle client non trouvé");
|
||||
}
|
||||
|
||||
roleService.deleteRole(existingRole.get().getId(), realmName, TypeRole.CLIENT_ROLE, clientId);
|
||||
}
|
||||
|
||||
// ==================== Endpoints Attribution de rôles ====================
|
||||
|
||||
@Override
|
||||
@POST
|
||||
@Path("/assign/realm/{userId}")
|
||||
@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());
|
||||
@RolesAllowed({ "admin", "role_manager" })
|
||||
public void assignRealmRoles(@PathParam("userId") String userId, @QueryParam("realm") String realmName,
|
||||
@NotNull RoleAssignmentRequestDTO request) {
|
||||
log.info("POST /api/roles/assign/realm/{} - Attribution de {} rôles", userId, request.getRoleNames().size());
|
||||
|
||||
try {
|
||||
RoleAssignmentDTO assignment = RoleAssignmentDTO.builder()
|
||||
RoleAssignmentDTO assignment = RoleAssignmentDTO.builder()
|
||||
.userId(userId)
|
||||
.roleNames(request.roleNames)
|
||||
.roleNames(request.getRoleNames())
|
||||
.typeRole(TypeRole.REALM_ROLE)
|
||||
.realmName(realmName)
|
||||
.build();
|
||||
roleService.assignRolesToUser(assignment);
|
||||
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();
|
||||
}
|
||||
roleService.assignRolesToUser(assignment);
|
||||
}
|
||||
|
||||
@Override
|
||||
@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());
|
||||
@RolesAllowed({ "admin", "role_manager" })
|
||||
public void revokeRealmRoles(@PathParam("userId") String userId, @QueryParam("realm") String realmName,
|
||||
@NotNull RoleAssignmentRequestDTO request) {
|
||||
log.info("POST /api/roles/revoke/realm/{} - Révocation de {} rôles", userId, request.getRoleNames().size());
|
||||
|
||||
try {
|
||||
RoleAssignmentDTO assignment = RoleAssignmentDTO.builder()
|
||||
RoleAssignmentDTO assignment = RoleAssignmentDTO.builder()
|
||||
.userId(userId)
|
||||
.roleNames(request.roleNames)
|
||||
.roleNames(request.getRoleNames())
|
||||
.typeRole(TypeRole.REALM_ROLE)
|
||||
.realmName(realmName)
|
||||
.build();
|
||||
roleService.revokeRolesFromUser(assignment);
|
||||
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();
|
||||
}
|
||||
roleService.revokeRolesFromUser(assignment);
|
||||
}
|
||||
|
||||
@Override
|
||||
@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
|
||||
) {
|
||||
@RolesAllowed({ "admin", "role_manager" })
|
||||
public void assignClientRoles(@PathParam("clientId") String clientId, @PathParam("userId") String userId,
|
||||
@QueryParam("realm") String realmName,
|
||||
@NotNull RoleAssignmentRequestDTO request) {
|
||||
log.info("POST /api/roles/assign/client/{}/{} - Attribution de {} rôles client",
|
||||
clientId, userId, request.roleNames.size());
|
||||
clientId, userId, request.getRoleNames().size());
|
||||
|
||||
try {
|
||||
RoleAssignmentDTO assignment = RoleAssignmentDTO.builder()
|
||||
RoleAssignmentDTO assignment = RoleAssignmentDTO.builder()
|
||||
.userId(userId)
|
||||
.roleNames(request.roleNames)
|
||||
.roleNames(request.getRoleNames())
|
||||
.typeRole(TypeRole.CLIENT_ROLE)
|
||||
.realmName(realmName)
|
||||
.clientName(clientId)
|
||||
.build();
|
||||
roleService.assignRolesToUser(assignment);
|
||||
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();
|
||||
}
|
||||
roleService.assignRolesToUser(assignment);
|
||||
}
|
||||
|
||||
@Override
|
||||
@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
|
||||
) {
|
||||
@RolesAllowed({ "admin", "role_manager", "role_viewer" })
|
||||
public List<RoleDTO> getUserRealmRoles(@PathParam("userId") String userId, @QueryParam("realm") String realmName) {
|
||||
log.info("GET /api/roles/user/realm/{} - realm: {}", userId, realmName);
|
||||
|
||||
try {
|
||||
List<RoleDTO> roles = roleService.getUserRealmRoles(userId, realmName);
|
||||
return Response.ok(roles).build();
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors de la récupération des rôles realm de l'utilisateur {}", userId, e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity(new ErrorResponse(e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
return roleService.getUserRealmRoles(userId, realmName);
|
||||
}
|
||||
|
||||
@Override
|
||||
@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
|
||||
) {
|
||||
@RolesAllowed({ "admin", "role_manager", "role_viewer" })
|
||||
public List<RoleDTO> getUserClientRoles(@PathParam("clientId") String clientId, @PathParam("userId") String userId,
|
||||
@QueryParam("realm") String realmName) {
|
||||
log.info("GET /api/roles/user/client/{}/{} - realm: {}", clientId, userId, realmName);
|
||||
|
||||
try {
|
||||
List<RoleDTO> roles = roleService.getUserClientRoles(userId, clientId, realmName);
|
||||
return Response.ok(roles).build();
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors de la récupération des rôles client de l'utilisateur {}", userId, e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity(new ErrorResponse(e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
return roleService.getUserClientRoles(userId, clientId, realmName);
|
||||
}
|
||||
|
||||
// ==================== Endpoints Rôles composites ====================
|
||||
|
||||
@Override
|
||||
@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());
|
||||
@RolesAllowed({ "admin", "role_manager" })
|
||||
public void addComposites(@PathParam("roleName") String roleName, @QueryParam("realm") String realmName,
|
||||
@NotNull RoleAssignmentRequestDTO request) {
|
||||
log.info("POST /api/roles/composite/{}/add - Ajout de {} composites", roleName, request.getRoleNames().size());
|
||||
|
||||
try {
|
||||
// Récupérer l'ID du rôle parent par son nom
|
||||
Optional<RoleDTO> parentRole = roleService.getRoleByName(roleName, realmName, TypeRole.REALM_ROLE, null);
|
||||
if (parentRole.isEmpty()) {
|
||||
return Response.status(Response.Status.NOT_FOUND)
|
||||
.entity(new ErrorResponse("Rôle parent non trouvé"))
|
||||
.build();
|
||||
}
|
||||
|
||||
// Convertir les noms de rôles en IDs
|
||||
List<String> childRoleIds = request.roleNames.stream()
|
||||
Optional<RoleDTO> parentRole = roleService.getRoleByName(roleName, realmName, TypeRole.REALM_ROLE, null);
|
||||
if (parentRole.isEmpty()) {
|
||||
throw new RuntimeException("Rôle parent non trouvé");
|
||||
}
|
||||
|
||||
List<String> childRoleIds = request.getRoleNames().stream()
|
||||
.map(name -> {
|
||||
Optional<RoleDTO> role = roleService.getRoleByName(name, realmName, TypeRole.REALM_ROLE, null);
|
||||
return role.map(RoleDTO::getId).orElse(null);
|
||||
})
|
||||
.filter(id -> id != null)
|
||||
.collect(java.util.stream.Collectors.toList());
|
||||
|
||||
roleService.addCompositeRoles(parentRole.get().getId(), childRoleIds, realmName, TypeRole.REALM_ROLE, null);
|
||||
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();
|
||||
}
|
||||
.collect(Collectors.toList());
|
||||
|
||||
roleService.addCompositeRoles(parentRole.get().getId(), childRoleIds, realmName, TypeRole.REALM_ROLE, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
@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
|
||||
) {
|
||||
@RolesAllowed({ "admin", "role_manager", "role_viewer" })
|
||||
public List<RoleDTO> getComposites(@PathParam("roleName") String roleName, @QueryParam("realm") String realmName) {
|
||||
log.info("GET /api/roles/composite/{} - realm: {}", roleName, realmName);
|
||||
|
||||
try {
|
||||
// Récupérer l'ID du rôle par son nom
|
||||
Optional<RoleDTO> role = roleService.getRoleByName(roleName, realmName, TypeRole.REALM_ROLE, null);
|
||||
if (role.isEmpty()) {
|
||||
return Response.status(Response.Status.NOT_FOUND)
|
||||
.entity(new ErrorResponse("Rôle non trouvé"))
|
||||
.build();
|
||||
}
|
||||
|
||||
List<RoleDTO> composites = roleService.getCompositeRoles(role.get().getId(), realmName, TypeRole.REALM_ROLE, null);
|
||||
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();
|
||||
Optional<RoleDTO> role = roleService.getRoleByName(roleName, realmName, TypeRole.REALM_ROLE, null);
|
||||
if (role.isEmpty()) {
|
||||
throw new RuntimeException("Rôle non trouvé");
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== DTOs internes ====================
|
||||
|
||||
@Schema(description = "Requête d'attribution/révocation de rôles")
|
||||
public static class RoleAssignmentRequest {
|
||||
@Schema(description = "Liste des noms de rôles", required = true)
|
||||
public List<String> roleNames;
|
||||
}
|
||||
|
||||
@Schema(description = "Réponse d'erreur")
|
||||
public static class ErrorResponse {
|
||||
@Schema(description = "Message d'erreur")
|
||||
public String message;
|
||||
|
||||
public ErrorResponse(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
return roleService.getCompositeRoles(role.get().getId(), realmName, TypeRole.REALM_ROLE, null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,318 +1,166 @@
|
||||
package dev.lions.user.manager.resource;
|
||||
|
||||
import dev.lions.user.manager.dto.role.RoleDTO;
|
||||
import dev.lions.user.manager.dto.user.UserDTO;
|
||||
import dev.lions.user.manager.api.SyncResourceApi;
|
||||
import dev.lions.user.manager.dto.sync.HealthStatusDTO;
|
||||
import dev.lions.user.manager.dto.sync.SyncConsistencyDTO;
|
||||
import dev.lions.user.manager.dto.sync.SyncHistoryDTO;
|
||||
import dev.lions.user.manager.dto.sync.SyncResultDTO;
|
||||
import dev.lions.user.manager.service.SyncService;
|
||||
import jakarta.annotation.security.PermitAll;
|
||||
import jakarta.annotation.security.RolesAllowed;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.ws.rs.*;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.Path;
|
||||
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;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* REST Resource pour la synchronisation avec Keycloak
|
||||
* REST Resource pour la synchronisation avec Keycloak.
|
||||
* Suit le même pattern que AuditResource : les annotations JAX-RS des méthodes
|
||||
* héritées de l'interface ne sont PAS répétées ici (conformité RESTEasy Reactive).
|
||||
*/
|
||||
@Path("/api/sync")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Tag(name = "Sync", description = "Synchronisation avec Keycloak et health checks")
|
||||
@Slf4j
|
||||
public class SyncResource {
|
||||
@jakarta.enterprise.context.ApplicationScoped
|
||||
@jakarta.ws.rs.Path("/api/sync")
|
||||
@jakarta.ws.rs.Produces(jakarta.ws.rs.core.MediaType.APPLICATION_JSON)
|
||||
@jakarta.ws.rs.Consumes(jakarta.ws.rs.core.MediaType.APPLICATION_JSON)
|
||||
public class SyncResource implements SyncResourceApi {
|
||||
|
||||
@Inject
|
||||
SyncService syncService;
|
||||
|
||||
@POST
|
||||
@Path("/users/{realmName}")
|
||||
@Operation(summary = "Synchroniser les utilisateurs", description = "Synchronise tous les utilisateurs depuis Keycloak")
|
||||
@APIResponses({
|
||||
@APIResponse(responseCode = "200", description = "Utilisateurs synchronisés"),
|
||||
@APIResponse(responseCode = "500", description = "Erreur serveur")
|
||||
})
|
||||
@RolesAllowed({"admin", "sync_manager"})
|
||||
public Response syncUsers(
|
||||
@Parameter(description = "Nom du realm") @PathParam("realmName") @NotBlank String realmName
|
||||
) {
|
||||
log.info("POST /api/sync/users/{} - Synchronisation des utilisateurs", realmName);
|
||||
@GET
|
||||
@Path("/ping")
|
||||
@PermitAll
|
||||
public String ping() {
|
||||
return "{\"status\":\"pong\",\"resource\":\"SyncResource\"}";
|
||||
}
|
||||
|
||||
@Override
|
||||
@PermitAll
|
||||
public HealthStatusDTO checkKeycloakHealth() {
|
||||
log.info("REST: checkKeycloakHealth sur /api/sync/health/keycloak");
|
||||
try {
|
||||
boolean available = syncService.isKeycloakAvailable();
|
||||
Map<String, Object> details = syncService.getKeycloakHealthInfo();
|
||||
return HealthStatusDTO.builder()
|
||||
.keycloakAccessible(available)
|
||||
.overallHealthy(available)
|
||||
.keycloakVersion((String) details.getOrDefault("version", "Unknown"))
|
||||
.timestamp(System.currentTimeMillis())
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors du check health keycloak", e);
|
||||
return HealthStatusDTO.builder()
|
||||
.overallHealthy(false)
|
||||
.errorMessage("Erreur: " + e.getMessage())
|
||||
.timestamp(System.currentTimeMillis())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "sync_manager" })
|
||||
public SyncResultDTO syncUsers(String realmName) {
|
||||
log.info("REST: syncUsers pour le realm: {}", realmName);
|
||||
long start = System.currentTimeMillis();
|
||||
try {
|
||||
int count = syncService.syncUsersFromRealm(realmName);
|
||||
return Response.ok(new SyncUsersResponse(count, null)).build();
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors de la synchronisation des utilisateurs du realm {}", realmName, e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity(new ErrorResponse(e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/roles/realm/{realmName}")
|
||||
@Operation(summary = "Synchroniser les rôles realm", description = "Synchronise tous les rôles realm depuis Keycloak")
|
||||
@APIResponses({
|
||||
@APIResponse(responseCode = "200", description = "Rôles realm synchronisés"),
|
||||
@APIResponse(responseCode = "500", description = "Erreur serveur")
|
||||
})
|
||||
@RolesAllowed({"admin", "sync_manager"})
|
||||
public Response syncRealmRoles(
|
||||
@PathParam("realmName") @NotBlank String realmName
|
||||
) {
|
||||
log.info("POST /api/sync/roles/realm/{} - Synchronisation des rôles realm", realmName);
|
||||
|
||||
try {
|
||||
int count = syncService.syncRolesFromRealm(realmName);
|
||||
return Response.ok(new SyncRolesResponse(count, null)).build();
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors de la synchronisation des rôles realm du realm {}", realmName, e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity(new ErrorResponse(e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/roles/client/{clientId}/{realmName}")
|
||||
@Operation(summary = "Synchroniser les rôles client", description = "Synchronise tous les rôles d'un client depuis Keycloak")
|
||||
@APIResponses({
|
||||
@APIResponse(responseCode = "200", description = "Rôles client synchronisés"),
|
||||
@APIResponse(responseCode = "500", description = "Erreur serveur")
|
||||
})
|
||||
@RolesAllowed({"admin", "sync_manager"})
|
||||
public Response syncClientRoles(
|
||||
@PathParam("clientId") @NotBlank String clientId,
|
||||
@PathParam("realmName") @NotBlank String realmName
|
||||
) {
|
||||
log.info("POST /api/sync/roles/client/{}/{} - Synchronisation des rôles client",
|
||||
clientId, realmName);
|
||||
|
||||
try {
|
||||
// Note: syncRolesFromRealm synchronise tous les rôles realm, pas les rôles client spécifiques
|
||||
// Pour les rôles client, on synchronise tous les rôles du realm (incluant les rôles client)
|
||||
int count = syncService.syncRolesFromRealm(realmName);
|
||||
return Response.ok(new SyncRolesResponse(count, null)).build();
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors de la synchronisation des rôles client du client {} (realm: {})",
|
||||
clientId, realmName, e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity(new ErrorResponse(e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/all/{realmName}")
|
||||
@Operation(summary = "Synchronisation complète", description = "Synchronise utilisateurs et rôles depuis Keycloak")
|
||||
@APIResponses({
|
||||
@APIResponse(responseCode = "200", description = "Synchronisation complète effectuée"),
|
||||
@APIResponse(responseCode = "500", description = "Erreur serveur")
|
||||
})
|
||||
@RolesAllowed({"admin", "sync_manager"})
|
||||
public Response syncAll(
|
||||
@PathParam("realmName") @NotBlank String realmName
|
||||
) {
|
||||
log.info("POST /api/sync/all/{} - Synchronisation complète", realmName);
|
||||
|
||||
try {
|
||||
Map<String, Object> result = syncService.forceSyncRealm(realmName);
|
||||
return Response.ok(result).build();
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors de la synchronisation complète du realm {}", realmName, e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity(new ErrorResponse(e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/health")
|
||||
@Operation(summary = "Vérifier la santé de Keycloak", description = "Retourne le statut de santé de Keycloak")
|
||||
@APIResponses({
|
||||
@APIResponse(responseCode = "200", description = "Statut de santé"),
|
||||
@APIResponse(responseCode = "503", description = "Keycloak non accessible")
|
||||
})
|
||||
@RolesAllowed({"admin", "sync_manager", "auditor"})
|
||||
public Response checkHealth() {
|
||||
log.info("GET /api/sync/health - Vérification de la santé de Keycloak");
|
||||
|
||||
try {
|
||||
boolean healthy = syncService.isKeycloakAvailable();
|
||||
if (healthy) {
|
||||
return Response.ok(new HealthCheckResponse(true, "Keycloak est accessible")).build();
|
||||
} else {
|
||||
return Response.status(Response.Status.SERVICE_UNAVAILABLE)
|
||||
.entity(new HealthCheckResponse(false, "Keycloak n'est pas accessible"))
|
||||
return SyncResultDTO.builder()
|
||||
.success(true)
|
||||
.usersCount(count)
|
||||
.realmName(realmName)
|
||||
.startTime(start)
|
||||
.endTime(System.currentTimeMillis())
|
||||
.build();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors de la vérification de santé de Keycloak", e);
|
||||
return Response.status(Response.Status.SERVICE_UNAVAILABLE)
|
||||
.entity(new HealthCheckResponse(false, e.getMessage()))
|
||||
.build();
|
||||
log.error("Erreur lors de la synchro users realm {}", realmName, e);
|
||||
return SyncResultDTO.builder()
|
||||
.success(false)
|
||||
.errorMessage(e.getMessage())
|
||||
.realmName(realmName)
|
||||
.startTime(start)
|
||||
.endTime(System.currentTimeMillis())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/health/detailed")
|
||||
@Operation(summary = "Statut de santé détaillé", description = "Retourne le statut de santé détaillé de Keycloak")
|
||||
@APIResponses({
|
||||
@APIResponse(responseCode = "200", description = "Statut détaillé"),
|
||||
@APIResponse(responseCode = "500", description = "Erreur serveur")
|
||||
})
|
||||
@RolesAllowed({"admin", "sync_manager"})
|
||||
public Response getDetailedHealthStatus() {
|
||||
log.info("GET /api/sync/health/detailed - Statut de santé détaillé");
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "sync_manager" })
|
||||
public SyncResultDTO syncRoles(String realmName, String clientName) {
|
||||
log.info("REST: syncRoles pour le realm: {}, client: {}", realmName, clientName);
|
||||
long start = System.currentTimeMillis();
|
||||
try {
|
||||
Map<String, Object> status = syncService.getKeycloakHealthInfo();
|
||||
return Response.ok(status).build(); // status est maintenant une Map<String, Object>
|
||||
int count = syncService.syncRolesFromRealm(realmName);
|
||||
return SyncResultDTO.builder()
|
||||
.success(true)
|
||||
.realmRolesCount(count)
|
||||
.realmName(realmName)
|
||||
.startTime(start)
|
||||
.endTime(System.currentTimeMillis())
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors de la récupération du statut de santé détaillé", e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity(new ErrorResponse(e.getMessage()))
|
||||
.build();
|
||||
log.error("Erreur lors de la synchro roles realm {}", realmName, e);
|
||||
return SyncResultDTO.builder()
|
||||
.success(false)
|
||||
.errorMessage(e.getMessage())
|
||||
.realmName(realmName)
|
||||
.startTime(start)
|
||||
.endTime(System.currentTimeMillis())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/check/realm/{realmName}")
|
||||
@Operation(summary = "Vérifier l'existence d'un realm", description = "Vérifie si un realm existe")
|
||||
@APIResponses({
|
||||
@APIResponse(responseCode = "200", description = "Résultat de la vérification"),
|
||||
@APIResponse(responseCode = "500", description = "Erreur serveur")
|
||||
})
|
||||
@RolesAllowed({"admin", "sync_manager"})
|
||||
public Response checkRealmExists(
|
||||
@PathParam("realmName") @NotBlank String realmName
|
||||
) {
|
||||
log.info("GET /api/sync/check/realm/{} - Vérification de l'existence", realmName);
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "sync_manager" })
|
||||
public SyncConsistencyDTO checkDataConsistency(String realmName) {
|
||||
log.info("REST: checkDataConsistency pour realm: {}", realmName);
|
||||
try {
|
||||
// Vérifier l'existence du realm en essayant de synchroniser (si ça marche, le realm existe)
|
||||
boolean exists = false;
|
||||
try {
|
||||
syncService.syncUsersFromRealm(realmName);
|
||||
exists = true;
|
||||
} catch (Exception e) {
|
||||
exists = false;
|
||||
}
|
||||
return Response.ok(new ExistsCheckResponse(exists, "realm", realmName)).build();
|
||||
Map<String, Object> report = syncService.checkDataConsistency(realmName);
|
||||
return SyncConsistencyDTO.builder()
|
||||
.realmName((String) report.get("realmName"))
|
||||
.status((String) report.get("status"))
|
||||
.usersKeycloakCount((Integer) report.get("usersKeycloakCount"))
|
||||
.usersLocalCount((Integer) report.get("usersLocalCount"))
|
||||
.error((String) report.get("error"))
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors de la vérification du realm {}", realmName, e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity(new ErrorResponse(e.getMessage()))
|
||||
.build();
|
||||
log.error("Erreur checkDataConsistency realm {}", realmName, e);
|
||||
return SyncConsistencyDTO.builder()
|
||||
.realmName(realmName)
|
||||
.status("ERROR")
|
||||
.error(e.getMessage())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/check/user/{userId}")
|
||||
@Operation(summary = "Vérifier l'existence d'un utilisateur", description = "Vérifie si un utilisateur existe")
|
||||
@APIResponses({
|
||||
@APIResponse(responseCode = "200", description = "Résultat de la vérification"),
|
||||
@APIResponse(responseCode = "500", description = "Erreur serveur")
|
||||
})
|
||||
@RolesAllowed({"admin", "sync_manager"})
|
||||
public Response checkUserExists(
|
||||
@PathParam("userId") @NotBlank String userId,
|
||||
@QueryParam("realm") @NotBlank String realmName
|
||||
) {
|
||||
log.info("GET /api/sync/check/user/{} - realm: {}", userId, realmName);
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "sync_manager", "user_viewer" })
|
||||
public SyncHistoryDTO getLastSyncStatus(String realmName) {
|
||||
log.info("REST: getLastSyncStatus pour realm: {}", realmName);
|
||||
return SyncHistoryDTO.builder()
|
||||
.realmName(realmName)
|
||||
.status("NEVER_SYNCED")
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "sync_manager" })
|
||||
public SyncHistoryDTO forceSyncRealm(String realmName) {
|
||||
log.info("REST: forceSyncRealm pour realm: {}", realmName);
|
||||
try {
|
||||
// Vérifier l'existence de l'utilisateur n'est plus disponible directement
|
||||
// On retourne false car cette fonctionnalité n'est plus dans l'interface
|
||||
boolean exists = false;
|
||||
return Response.ok(new ExistsCheckResponse(exists, "user", userId)).build();
|
||||
syncService.forceSyncRealm(realmName);
|
||||
return SyncHistoryDTO.builder()
|
||||
.realmName(realmName)
|
||||
.status("SUCCESS")
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors de la vérification de l'utilisateur {} dans le realm {}",
|
||||
userId, realmName, e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity(new ErrorResponse(e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== DTOs internes ====================
|
||||
|
||||
@Schema(description = "Réponse de synchronisation d'utilisateurs")
|
||||
public static class SyncUsersResponse {
|
||||
@Schema(description = "Nombre d'utilisateurs synchronisés")
|
||||
public int count;
|
||||
|
||||
@Schema(description = "Liste des utilisateurs synchronisés")
|
||||
public List<UserDTO> users;
|
||||
|
||||
public SyncUsersResponse(int count, List<UserDTO> users) {
|
||||
this.count = count;
|
||||
this.users = users;
|
||||
}
|
||||
}
|
||||
|
||||
@Schema(description = "Réponse de synchronisation de rôles")
|
||||
public static class SyncRolesResponse {
|
||||
@Schema(description = "Nombre de rôles synchronisés")
|
||||
public int count;
|
||||
|
||||
@Schema(description = "Liste des rôles synchronisés")
|
||||
public List<RoleDTO> roles;
|
||||
|
||||
public SyncRolesResponse(int count, List<RoleDTO> roles) {
|
||||
this.count = count;
|
||||
this.roles = roles;
|
||||
}
|
||||
}
|
||||
|
||||
@Schema(description = "Réponse de vérification de santé")
|
||||
public static class HealthCheckResponse {
|
||||
@Schema(description = "Indique si Keycloak est accessible")
|
||||
public boolean healthy;
|
||||
|
||||
@Schema(description = "Message descriptif")
|
||||
public String message;
|
||||
|
||||
public HealthCheckResponse(boolean healthy, String message) {
|
||||
this.healthy = healthy;
|
||||
this.message = message;
|
||||
}
|
||||
}
|
||||
|
||||
@Schema(description = "Réponse de vérification d'existence")
|
||||
public static class ExistsCheckResponse {
|
||||
@Schema(description = "Indique si la ressource existe")
|
||||
public boolean exists;
|
||||
|
||||
@Schema(description = "Type de ressource (realm, user, client, etc.)")
|
||||
public String resourceType;
|
||||
|
||||
@Schema(description = "Identifiant de la ressource")
|
||||
public String resourceId;
|
||||
|
||||
public ExistsCheckResponse(boolean exists, String resourceType, String resourceId) {
|
||||
this.exists = exists;
|
||||
this.resourceType = resourceType;
|
||||
this.resourceId = resourceId;
|
||||
}
|
||||
}
|
||||
|
||||
@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;
|
||||
log.error("Erreur forceSyncRealm realm {}", realmName, e);
|
||||
return SyncHistoryDTO.builder()
|
||||
.realmName(realmName)
|
||||
.status("FAILED")
|
||||
.build();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
package dev.lions.user.manager.resource;
|
||||
|
||||
import dev.lions.user.manager.api.UserMetricsResourceApi;
|
||||
import dev.lions.user.manager.client.KeycloakAdminClient;
|
||||
import dev.lions.user.manager.dto.common.UserSessionStatsDTO;
|
||||
import jakarta.annotation.security.RolesAllowed;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.ws.rs.Path;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.keycloak.admin.client.resource.RealmResource;
|
||||
import org.keycloak.admin.client.resource.UserResource;
|
||||
import org.keycloak.admin.client.resource.UsersResource;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Ressource REST fournissant des métriques agrégées sur les utilisateurs.
|
||||
* Implémente l'interface API commune.
|
||||
*
|
||||
* Toutes les valeurs sont calculées en temps réel à partir de Keycloak
|
||||
* (aucune approximation ni cache local).
|
||||
*/
|
||||
@Slf4j
|
||||
@ApplicationScoped
|
||||
@Path("/api/metrics/users")
|
||||
public class UserMetricsResource implements UserMetricsResourceApi {
|
||||
|
||||
@Inject
|
||||
KeycloakAdminClient keycloakAdminClient;
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "user_manager", "auditor" })
|
||||
public UserSessionStatsDTO getUserSessionStats(String realmName) {
|
||||
String effectiveRealm = (realmName == null || realmName.isBlank()) ? "master" : realmName;
|
||||
log.info("GET /api/metrics/users/sessions - realm={}", effectiveRealm);
|
||||
|
||||
try {
|
||||
RealmResource realm = keycloakAdminClient.getRealm(effectiveRealm);
|
||||
UsersResource usersResource = realm.users();
|
||||
|
||||
// Liste complète des utilisateurs du realm (source de vérité Keycloak)
|
||||
List<UserRepresentation> users = usersResource.list();
|
||||
long totalUsers = users.size();
|
||||
|
||||
long activeSessions = 0L;
|
||||
long onlineUsers = 0L;
|
||||
|
||||
for (UserRepresentation user : users) {
|
||||
UserResource userResource = usersResource.get(user.getId());
|
||||
int sessionsForUser = userResource.getUserSessions().size();
|
||||
|
||||
activeSessions += sessionsForUser;
|
||||
if (sessionsForUser > 0) {
|
||||
onlineUsers++;
|
||||
}
|
||||
}
|
||||
|
||||
return UserSessionStatsDTO.builder()
|
||||
.realmName(effectiveRealm)
|
||||
.totalUsers(totalUsers)
|
||||
.activeSessions(activeSessions)
|
||||
.onlineUsers(onlineUsers)
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors du calcul des statistiques de sessions pour le realm {}", effectiveRealm, e);
|
||||
// On laisse l'exception remonter pour signaler une vraie erreur (pas de valeur approximative)
|
||||
throw new RuntimeException("Impossible de calculer les statistiques de sessions en temps réel", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,136 +1,62 @@
|
||||
package dev.lions.user.manager.resource;
|
||||
|
||||
import dev.lions.user.manager.dto.user.UserDTO;
|
||||
import dev.lions.user.manager.dto.user.UserSearchCriteriaDTO;
|
||||
import dev.lions.user.manager.dto.user.UserSearchResultDTO;
|
||||
import dev.lions.user.manager.api.UserResourceApi;
|
||||
import dev.lions.user.manager.dto.common.ApiErrorDTO;
|
||||
import dev.lions.user.manager.dto.importexport.ImportResultDTO;
|
||||
import dev.lions.user.manager.dto.user.*;
|
||||
import dev.lions.user.manager.service.UserService;
|
||||
import jakarta.annotation.security.RolesAllowed;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.ws.rs.*;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.POST;
|
||||
import jakarta.ws.rs.QueryParam;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.eclipse.microprofile.openapi.annotations.Operation;
|
||||
import org.eclipse.microprofile.openapi.annotations.media.Content;
|
||||
import org.eclipse.microprofile.openapi.annotations.media.Schema;
|
||||
import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
|
||||
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
|
||||
import org.eclipse.microprofile.openapi.annotations.responses.APIResponses;
|
||||
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* REST Resource pour la gestion des utilisateurs
|
||||
* Endpoints exposés pour les opérations CRUD sur les utilisateurs Keycloak
|
||||
* Implémente l'interface API commune.
|
||||
*/
|
||||
@Path("/api/users")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Tag(name = "Users", description = "Gestion des utilisateurs Keycloak")
|
||||
@Slf4j
|
||||
public class UserResource {
|
||||
@jakarta.enterprise.context.ApplicationScoped
|
||||
@jakarta.ws.rs.Path("/api/users")
|
||||
public class UserResource implements UserResourceApi {
|
||||
|
||||
@Inject
|
||||
UserService userService;
|
||||
|
||||
@POST
|
||||
@Path("/search")
|
||||
@Operation(summary = "Rechercher des utilisateurs", description = "Recherche d'utilisateurs selon des critères")
|
||||
@APIResponses({
|
||||
@APIResponse(responseCode = "200", description = "Résultats de recherche",
|
||||
content = @Content(schema = @Schema(implementation = UserSearchResultDTO.class))),
|
||||
@APIResponse(responseCode = "400", description = "Critères invalides"),
|
||||
@APIResponse(responseCode = "500", description = "Erreur serveur")
|
||||
})
|
||||
@RolesAllowed({"admin", "user_manager"})
|
||||
public Response searchUsers(@Valid @NotNull UserSearchCriteriaDTO criteria) {
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "user_manager" })
|
||||
public UserSearchResultDTO searchUsers(@Valid @NotNull UserSearchCriteriaDTO criteria) {
|
||||
log.info("POST /api/users/search - Recherche d'utilisateurs");
|
||||
|
||||
try {
|
||||
UserSearchResultDTO result = userService.searchUsers(criteria);
|
||||
return Response.ok(result).build();
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors de la recherche d'utilisateurs", e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity(new ErrorResponse(e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
return userService.searchUsers(criteria);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/{userId}")
|
||||
@Operation(summary = "Récupérer un utilisateur par ID", description = "Récupère les détails d'un utilisateur")
|
||||
@APIResponses({
|
||||
@APIResponse(responseCode = "200", description = "Utilisateur trouvé",
|
||||
content = @Content(schema = @Schema(implementation = UserDTO.class))),
|
||||
@APIResponse(responseCode = "404", description = "Utilisateur non trouvé"),
|
||||
@APIResponse(responseCode = "500", description = "Erreur serveur")
|
||||
})
|
||||
@RolesAllowed({"admin", "user_manager", "user_viewer"})
|
||||
public Response getUserById(
|
||||
@Parameter(description = "ID de l'utilisateur") @PathParam("userId") @NotBlank String userId,
|
||||
@Parameter(description = "Nom du realm") @QueryParam("realm") @NotBlank String realmName
|
||||
) {
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "user_manager", "user_viewer" })
|
||||
public UserDTO getUserById(String userId, String realmName) {
|
||||
log.info("GET /api/users/{} - realm: {}", userId, realmName);
|
||||
|
||||
try {
|
||||
return userService.getUserById(userId, realmName)
|
||||
.map(user -> Response.ok(user).build())
|
||||
.orElse(Response.status(Response.Status.NOT_FOUND)
|
||||
.entity(new ErrorResponse("Utilisateur non trouvé"))
|
||||
.build());
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors de la récupération de l'utilisateur {}", userId, e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity(new ErrorResponse(e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
return userService.getUserById(userId, realmName)
|
||||
.orElseThrow(() -> new RuntimeException("Utilisateur non trouvé")); // ExceptionMapper should handle/map
|
||||
// to 404
|
||||
}
|
||||
|
||||
@GET
|
||||
@Operation(summary = "Lister tous les utilisateurs", description = "Liste paginée de tous les utilisateurs")
|
||||
@APIResponses({
|
||||
@APIResponse(responseCode = "200", description = "Liste des utilisateurs",
|
||||
content = @Content(schema = @Schema(implementation = UserSearchResultDTO.class))),
|
||||
@APIResponse(responseCode = "500", description = "Erreur serveur")
|
||||
})
|
||||
@RolesAllowed({"admin", "user_manager", "user_viewer"})
|
||||
public Response getAllUsers(
|
||||
@QueryParam("realm") @NotBlank String realmName,
|
||||
@QueryParam("page") @DefaultValue("0") int page,
|
||||
@QueryParam("pageSize") @DefaultValue("20") int pageSize
|
||||
) {
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "user_manager", "user_viewer" })
|
||||
public UserSearchResultDTO getAllUsers(String realmName, int page, int pageSize) {
|
||||
log.info("GET /api/users - realm: {}, page: {}, pageSize: {}", realmName, page, pageSize);
|
||||
|
||||
try {
|
||||
UserSearchResultDTO result = userService.getAllUsers(realmName, page, pageSize);
|
||||
return Response.ok(result).build();
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors de la récupération des utilisateurs", e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity(new ErrorResponse(e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
return userService.getAllUsers(realmName, page, pageSize);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Operation(summary = "Créer un utilisateur", description = "Crée un nouvel utilisateur dans Keycloak")
|
||||
@APIResponses({
|
||||
@APIResponse(responseCode = "201", description = "Utilisateur créé",
|
||||
content = @Content(schema = @Schema(implementation = UserDTO.class))),
|
||||
@APIResponse(responseCode = "400", description = "Données invalides"),
|
||||
@APIResponse(responseCode = "409", description = "Utilisateur existe déjà"),
|
||||
@APIResponse(responseCode = "500", description = "Erreur serveur")
|
||||
})
|
||||
@RolesAllowed({"admin", "user_manager"})
|
||||
public Response createUser(
|
||||
@Valid @NotNull UserDTO user,
|
||||
@QueryParam("realm") @NotBlank String realmName
|
||||
) {
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "user_manager" })
|
||||
public Response createUser(@Valid @NotNull UserDTO user, String realmName) {
|
||||
log.info("POST /api/users - Création d'un utilisateur: {}", user.getUsername());
|
||||
|
||||
try {
|
||||
@@ -139,268 +65,97 @@ public class UserResource {
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.warn("Données invalides lors de la création: {}", e.getMessage());
|
||||
return Response.status(Response.Status.CONFLICT)
|
||||
.entity(new ErrorResponse(e.getMessage()))
|
||||
.build();
|
||||
.entity(new ApiErrorDTO(e.getMessage()))
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors de la création de l'utilisateur", e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity(new ErrorResponse(e.getMessage()))
|
||||
.build();
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@PUT
|
||||
@Path("/{userId}")
|
||||
@Operation(summary = "Mettre à jour un utilisateur", description = "Met à jour les informations d'un utilisateur")
|
||||
@APIResponses({
|
||||
@APIResponse(responseCode = "200", description = "Utilisateur mis à jour",
|
||||
content = @Content(schema = @Schema(implementation = UserDTO.class))),
|
||||
@APIResponse(responseCode = "404", description = "Utilisateur non trouvé"),
|
||||
@APIResponse(responseCode = "400", description = "Données invalides"),
|
||||
@APIResponse(responseCode = "500", description = "Erreur serveur")
|
||||
})
|
||||
@RolesAllowed({"admin", "user_manager"})
|
||||
public Response updateUser(
|
||||
@PathParam("userId") @NotBlank String userId,
|
||||
@Valid @NotNull UserDTO user,
|
||||
@QueryParam("realm") @NotBlank String realmName
|
||||
) {
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "user_manager" })
|
||||
public UserDTO updateUser(String userId, @Valid @NotNull UserDTO user, String realmName) {
|
||||
log.info("PUT /api/users/{} - Mise à jour", userId);
|
||||
|
||||
try {
|
||||
UserDTO updatedUser = userService.updateUser(userId, user, realmName);
|
||||
return Response.ok(updatedUser).build();
|
||||
} catch (RuntimeException e) {
|
||||
if (e.getMessage().contains("non trouvé")) {
|
||||
return Response.status(Response.Status.NOT_FOUND)
|
||||
.entity(new ErrorResponse(e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
log.error("Erreur lors de la mise à jour de l'utilisateur {}", userId, e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity(new ErrorResponse(e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
return userService.updateUser(userId, user, realmName);
|
||||
}
|
||||
|
||||
@DELETE
|
||||
@Path("/{userId}")
|
||||
@Operation(summary = "Supprimer un utilisateur", description = "Supprime un utilisateur (soft ou hard delete)")
|
||||
@APIResponses({
|
||||
@APIResponse(responseCode = "204", description = "Utilisateur supprimé"),
|
||||
@APIResponse(responseCode = "404", description = "Utilisateur non trouvé"),
|
||||
@APIResponse(responseCode = "500", description = "Erreur serveur")
|
||||
})
|
||||
@RolesAllowed({"admin"})
|
||||
public Response deleteUser(
|
||||
@PathParam("userId") @NotBlank String userId,
|
||||
@QueryParam("realm") @NotBlank String realmName,
|
||||
@QueryParam("hardDelete") @DefaultValue("false") boolean hardDelete
|
||||
) {
|
||||
@Override
|
||||
@RolesAllowed({ "admin" })
|
||||
public void deleteUser(String userId, String realmName, boolean hardDelete) {
|
||||
log.info("DELETE /api/users/{} - realm: {}, hardDelete: {}", userId, realmName, hardDelete);
|
||||
|
||||
try {
|
||||
userService.deleteUser(userId, realmName, hardDelete);
|
||||
return Response.noContent().build();
|
||||
} catch (RuntimeException e) {
|
||||
if (e.getMessage().contains("non trouvé")) {
|
||||
return Response.status(Response.Status.NOT_FOUND)
|
||||
.entity(new ErrorResponse(e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
log.error("Erreur lors de la suppression de l'utilisateur {}", userId, e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity(new ErrorResponse(e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
userService.deleteUser(userId, realmName, hardDelete);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/{userId}/activate")
|
||||
@Operation(summary = "Activer un utilisateur", description = "Active un utilisateur désactivé")
|
||||
@APIResponses({
|
||||
@APIResponse(responseCode = "204", description = "Utilisateur activé"),
|
||||
@APIResponse(responseCode = "404", description = "Utilisateur non trouvé"),
|
||||
@APIResponse(responseCode = "500", description = "Erreur serveur")
|
||||
})
|
||||
@RolesAllowed({"admin", "user_manager"})
|
||||
public Response activateUser(
|
||||
@PathParam("userId") @NotBlank String userId,
|
||||
@QueryParam("realm") @NotBlank String realmName
|
||||
) {
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "user_manager" })
|
||||
public void activateUser(String userId, String realmName) {
|
||||
log.info("POST /api/users/{}/activate", userId);
|
||||
|
||||
try {
|
||||
userService.activateUser(userId, realmName);
|
||||
return Response.noContent().build();
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors de l'activation de l'utilisateur {}", userId, e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity(new ErrorResponse(e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
userService.activateUser(userId, realmName);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/{userId}/deactivate")
|
||||
@Operation(summary = "Désactiver un utilisateur", description = "Désactive un utilisateur")
|
||||
@APIResponses({
|
||||
@APIResponse(responseCode = "204", description = "Utilisateur désactivé"),
|
||||
@APIResponse(responseCode = "404", description = "Utilisateur non trouvé"),
|
||||
@APIResponse(responseCode = "500", description = "Erreur serveur")
|
||||
})
|
||||
@RolesAllowed({"admin", "user_manager"})
|
||||
public Response deactivateUser(
|
||||
@PathParam("userId") @NotBlank String userId,
|
||||
@QueryParam("realm") @NotBlank String realmName,
|
||||
@QueryParam("raison") String raison
|
||||
) {
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "user_manager" })
|
||||
public void deactivateUser(String userId, String realmName, String raison) {
|
||||
log.info("POST /api/users/{}/deactivate - raison: {}", userId, raison);
|
||||
|
||||
try {
|
||||
userService.deactivateUser(userId, realmName, raison);
|
||||
return Response.noContent().build();
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors de la désactivation de l'utilisateur {}", userId, e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity(new ErrorResponse(e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
userService.deactivateUser(userId, realmName, raison);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/{userId}/reset-password")
|
||||
@Operation(summary = "Réinitialiser le mot de passe", description = "Définit un nouveau mot de passe pour l'utilisateur")
|
||||
@APIResponses({
|
||||
@APIResponse(responseCode = "204", description = "Mot de passe réinitialisé"),
|
||||
@APIResponse(responseCode = "404", description = "Utilisateur non trouvé"),
|
||||
@APIResponse(responseCode = "500", description = "Erreur serveur")
|
||||
})
|
||||
@RolesAllowed({"admin", "user_manager"})
|
||||
public Response resetPassword(
|
||||
@PathParam("userId") @NotBlank String userId,
|
||||
@QueryParam("realm") @NotBlank String realmName,
|
||||
@NotNull PasswordResetRequest request
|
||||
) {
|
||||
log.info("POST /api/users/{}/reset-password - temporary: {}", userId, request.temporary);
|
||||
|
||||
try {
|
||||
userService.resetPassword(userId, realmName, request.password, request.temporary);
|
||||
return Response.noContent().build();
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors de la réinitialisation du mot de passe pour {}", userId, e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity(new ErrorResponse(e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "user_manager" })
|
||||
public void resetPassword(String userId, String realmName, @NotNull PasswordResetRequestDTO request) {
|
||||
log.info("POST /api/users/{}/reset-password - temporary: {}", userId, request.isTemporary());
|
||||
userService.resetPassword(userId, realmName, request.getPassword(), request.isTemporary());
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/{userId}/send-verification-email")
|
||||
@Operation(summary = "Envoyer email de vérification", description = "Envoie un email de vérification à l'utilisateur")
|
||||
@APIResponses({
|
||||
@APIResponse(responseCode = "204", description = "Email envoyé"),
|
||||
@APIResponse(responseCode = "404", description = "Utilisateur non trouvé"),
|
||||
@APIResponse(responseCode = "500", description = "Erreur serveur")
|
||||
})
|
||||
@RolesAllowed({"admin", "user_manager"})
|
||||
public Response sendVerificationEmail(
|
||||
@PathParam("userId") @NotBlank String userId,
|
||||
@QueryParam("realm") @NotBlank String realmName
|
||||
) {
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "user_manager" })
|
||||
public void sendVerificationEmail(String userId, String realmName) {
|
||||
log.info("POST /api/users/{}/send-verification-email", userId);
|
||||
|
||||
try {
|
||||
userService.sendVerificationEmail(userId, realmName);
|
||||
return Response.noContent().build();
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors de l'envoi de l'email de vérification pour {}", userId, e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity(new ErrorResponse(e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
userService.sendVerificationEmail(userId, realmName);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/{userId}/logout-sessions")
|
||||
@Operation(summary = "Déconnecter toutes les sessions", description = "Révoque toutes les sessions actives de l'utilisateur")
|
||||
@APIResponses({
|
||||
@APIResponse(responseCode = "200", description = "Sessions révoquées"),
|
||||
@APIResponse(responseCode = "404", description = "Utilisateur non trouvé"),
|
||||
@APIResponse(responseCode = "500", description = "Erreur serveur")
|
||||
})
|
||||
@RolesAllowed({"admin", "user_manager"})
|
||||
public Response logoutAllSessions(
|
||||
@PathParam("userId") @NotBlank String userId,
|
||||
@QueryParam("realm") @NotBlank String realmName
|
||||
) {
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "user_manager" })
|
||||
public SessionsRevokedDTO logoutAllSessions(String userId, String realmName) {
|
||||
log.info("POST /api/users/{}/logout-sessions", userId);
|
||||
|
||||
try {
|
||||
int count = userService.logoutAllSessions(userId, realmName);
|
||||
return Response.ok(new SessionsRevokedResponse(count)).build();
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors de la déconnexion des sessions pour {}", userId, e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity(new ErrorResponse(e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
int count = userService.logoutAllSessions(userId, realmName);
|
||||
return new SessionsRevokedDTO(count);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/{userId}/sessions")
|
||||
@Operation(summary = "Récupérer les sessions actives", description = "Liste les sessions actives de l'utilisateur")
|
||||
@APIResponses({
|
||||
@APIResponse(responseCode = "200", description = "Liste des sessions"),
|
||||
@APIResponse(responseCode = "404", description = "Utilisateur non trouvé"),
|
||||
@APIResponse(responseCode = "500", description = "Erreur serveur")
|
||||
})
|
||||
@RolesAllowed({"admin", "user_manager", "user_viewer"})
|
||||
public Response getActiveSessions(
|
||||
@PathParam("userId") @NotBlank String userId,
|
||||
@QueryParam("realm") @NotBlank String realmName
|
||||
) {
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "user_manager", "user_viewer" })
|
||||
public List<String> getActiveSessions(String userId, String realmName) {
|
||||
log.info("GET /api/users/{}/sessions", userId);
|
||||
return userService.getActiveSessions(userId, realmName);
|
||||
}
|
||||
|
||||
try {
|
||||
List<String> sessions = userService.getActiveSessions(userId, realmName);
|
||||
return Response.ok(sessions).build();
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors de la récupération des sessions pour {}", userId, e);
|
||||
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
|
||||
.entity(new ErrorResponse(e.getMessage()))
|
||||
@Override
|
||||
@GET
|
||||
@jakarta.ws.rs.Path("/export/csv")
|
||||
@jakarta.ws.rs.Produces("text/csv")
|
||||
@RolesAllowed({ "admin", "user_manager" })
|
||||
public Response exportUsersToCSV(@QueryParam("realm") String realmName) {
|
||||
log.info("GET /api/users/export/csv - realm: {}", realmName);
|
||||
UserSearchCriteriaDTO criteria = UserSearchCriteriaDTO.builder()
|
||||
.realmName(realmName)
|
||||
.page(0)
|
||||
.pageSize(10_000)
|
||||
.build();
|
||||
String csv = userService.exportUsersToCSV(criteria);
|
||||
return Response.ok(csv)
|
||||
.type(MediaType.valueOf("text/csv"))
|
||||
.header("Content-Disposition", "attachment; filename=\"users-" + (realmName != null ? realmName : "export") + ".csv\"")
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== DTOs internes ====================
|
||||
|
||||
@Schema(description = "Requête de réinitialisation de mot de passe")
|
||||
public static class PasswordResetRequest {
|
||||
@Schema(description = "Nouveau mot de passe", required = true)
|
||||
public String password;
|
||||
|
||||
@Schema(description = "Indique si le mot de passe est temporaire", defaultValue = "true")
|
||||
public boolean temporary = true;
|
||||
}
|
||||
|
||||
@Schema(description = "Réponse de révocation de sessions")
|
||||
public static class SessionsRevokedResponse {
|
||||
@Schema(description = "Nombre de sessions révoquées")
|
||||
public int count;
|
||||
|
||||
public SessionsRevokedResponse(int count) {
|
||||
this.count = count;
|
||||
}
|
||||
}
|
||||
|
||||
@Schema(description = "Réponse d'erreur")
|
||||
public static class ErrorResponse {
|
||||
@Schema(description = "Message d'erreur")
|
||||
public String message;
|
||||
|
||||
public ErrorResponse(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
@Override
|
||||
@POST
|
||||
@jakarta.ws.rs.Path("/import/csv")
|
||||
@jakarta.ws.rs.Consumes(MediaType.TEXT_PLAIN)
|
||||
@RolesAllowed({ "admin", "user_manager" })
|
||||
public ImportResultDTO importUsersFromCSV(@QueryParam("realm") String realmName, String csvContent) {
|
||||
log.info("POST /api/users/import/csv - realm: {}", realmName);
|
||||
return userService.importUsersFromCSV(csvContent, realmName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package dev.lions.user.manager.security;
|
||||
|
||||
import io.quarkus.security.identity.AuthenticationRequestContext;
|
||||
import io.quarkus.security.identity.SecurityIdentity;
|
||||
import io.quarkus.security.identity.SecurityIdentityAugmentor;
|
||||
import io.quarkus.security.runtime.QuarkusSecurityIdentity;
|
||||
import io.quarkus.arc.profile.IfBuildProfile;
|
||||
import io.smallrye.mutiny.Uni;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Augmenteur de sécurité pour le mode DEV
|
||||
* Ajoute automatiquement les rôles admin et user_manager à toutes les requêtes
|
||||
* Permet de tester l'API sans authentification Keycloak
|
||||
*/
|
||||
@ApplicationScoped
|
||||
@IfBuildProfile("dev")
|
||||
public class DevModeSecurityAugmentor implements SecurityIdentityAugmentor {
|
||||
|
||||
@ConfigProperty(name = "quarkus.oidc.enabled", defaultValue = "true")
|
||||
boolean oidcEnabled;
|
||||
|
||||
@Override
|
||||
public Uni<SecurityIdentity> augment(SecurityIdentity identity, AuthenticationRequestContext context) {
|
||||
// Seulement actif si OIDC est désactivé (mode DEV)
|
||||
if (!oidcEnabled && identity.isAnonymous()) {
|
||||
// Créer une identité avec les rôles nécessaires pour DEV
|
||||
return Uni.createFrom().item(QuarkusSecurityIdentity.builder(identity)
|
||||
.setPrincipal(() -> "dev-user")
|
||||
.addRoles(Set.of("admin", "user_manager", "user_viewer"))
|
||||
.build());
|
||||
}
|
||||
return Uni.createFrom().item(identity);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
package dev.lions.user.manager.server.impl.entity;
|
||||
|
||||
import dev.lions.user.manager.enums.audit.TypeActionAudit;
|
||||
import io.quarkus.hibernate.orm.panache.PanacheEntity;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* Entité JPA pour la persistance des logs d'audit en base de données PostgreSQL.
|
||||
*
|
||||
* <p>Cette entité représente un enregistrement d'audit qui track toutes les actions
|
||||
* effectuées sur les utilisateurs du système (création, modification, suppression, etc.).</p>
|
||||
*
|
||||
* <p><b>Utilisation:</b></p>
|
||||
* <pre>
|
||||
* AuditLogEntity auditLog = new AuditLogEntity();
|
||||
* auditLog.setUserId("user-123");
|
||||
* auditLog.setAction(TypeActionAudit.CREATION_UTILISATEUR);
|
||||
* auditLog.setDetails("Utilisateur créé avec succès");
|
||||
* auditLog.setAuteurAction("admin");
|
||||
* auditLog.setTimestamp(LocalDateTime.now());
|
||||
* auditLog.persist();
|
||||
* </pre>
|
||||
*
|
||||
* @see dev.lions.user.manager.server.api.dto.AuditLogDTO
|
||||
* @see TypeActionAudit
|
||||
* @author Lions Development Team
|
||||
* @version 1.0.0
|
||||
* @since 2026-01-02
|
||||
*/
|
||||
@Entity
|
||||
@Table(
|
||||
name = "audit_logs",
|
||||
indexes = {
|
||||
@Index(name = "idx_audit_user_id", columnList = "user_id"),
|
||||
@Index(name = "idx_audit_action", columnList = "action"),
|
||||
@Index(name = "idx_audit_timestamp", columnList = "timestamp"),
|
||||
@Index(name = "idx_audit_auteur", columnList = "auteur_action")
|
||||
}
|
||||
)
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class AuditLogEntity extends PanacheEntity {
|
||||
|
||||
/**
|
||||
* ID de l'utilisateur concerné par l'action.
|
||||
* <p>Peut être null pour les actions système qui ne concernent pas un utilisateur spécifique.</p>
|
||||
*/
|
||||
@Column(name = "user_id", length = 255)
|
||||
private String userId;
|
||||
|
||||
/**
|
||||
* Type d'action effectuée (CREATION_UTILISATEUR, MODIFICATION_UTILISATEUR, etc.).
|
||||
* <p>Stocké en tant que STRING pour faciliter la lecture en base de données.</p>
|
||||
*/
|
||||
@Column(name = "action", nullable = false, length = 100)
|
||||
@Enumerated(EnumType.STRING)
|
||||
private TypeActionAudit action;
|
||||
|
||||
/**
|
||||
* Détails complémentaires sur l'action effectuée.
|
||||
* <p>Peut contenir des informations contextuelles comme les champs modifiés,
|
||||
* les raisons d'une action, ou des messages d'erreur.</p>
|
||||
*/
|
||||
@Column(name = "details", columnDefinition = "TEXT")
|
||||
private String details;
|
||||
|
||||
/**
|
||||
* Identifiant de l'utilisateur qui a effectué l'action.
|
||||
* <p>Généralement l'username ou l'ID de l'administrateur/utilisateur connecté.</p>
|
||||
*/
|
||||
@Column(name = "auteur_action", nullable = false, length = 255)
|
||||
private String auteurAction;
|
||||
|
||||
/**
|
||||
* Timestamp précis de l'action.
|
||||
* <p>Utilisé pour l'ordre chronologique des logs et le filtrage temporel.</p>
|
||||
*/
|
||||
@Column(name = "timestamp", nullable = false)
|
||||
private LocalDateTime timestamp;
|
||||
|
||||
/**
|
||||
* Adresse IP de l'auteur de l'action.
|
||||
* <p>Utile pour la traçabilité et la détection d'anomalies.</p>
|
||||
*/
|
||||
@Column(name = "ip_address", length = 45)
|
||||
private String ipAddress;
|
||||
|
||||
/**
|
||||
* User-Agent du client (navigateur, application, etc.).
|
||||
* <p>Permet d'identifier le type de client utilisé pour l'action.</p>
|
||||
*/
|
||||
@Column(name = "user_agent", length = 500)
|
||||
private String userAgent;
|
||||
|
||||
/**
|
||||
* Nom du realm Keycloak concerné.
|
||||
* <p>Important dans un environnement multi-tenant pour isoler les logs par realm.</p>
|
||||
*/
|
||||
@Column(name = "realm_name", length = 255)
|
||||
private String realmName;
|
||||
|
||||
/**
|
||||
* Indique si l'action a réussi ou échoué.
|
||||
* <p>Permet de filtrer facilement les actions en erreur pour analyse.</p>
|
||||
*/
|
||||
@Column(name = "success", nullable = false)
|
||||
private Boolean success = true;
|
||||
|
||||
/**
|
||||
* Message d'erreur en cas d'échec de l'action.
|
||||
* <p>Null si success = true.</p>
|
||||
*/
|
||||
@Column(name = "error_message", columnDefinition = "TEXT")
|
||||
private String errorMessage;
|
||||
|
||||
/**
|
||||
* Constructeur par défaut requis par JPA.
|
||||
*/
|
||||
public AuditLogEntity() {
|
||||
this.timestamp = LocalDateTime.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* Recherche tous les logs d'audit pour un utilisateur donné.
|
||||
*
|
||||
* @param userId ID de l'utilisateur
|
||||
* @return Liste des logs triés par timestamp décroissant
|
||||
*/
|
||||
public static java.util.List<AuditLogEntity> findByUserId(String userId) {
|
||||
return list("userId = ?1 order by timestamp desc", userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recherche tous les logs d'audit d'un type d'action donné.
|
||||
*
|
||||
* @param action Type d'action
|
||||
* @return Liste des logs triés par timestamp décroissant
|
||||
*/
|
||||
public static java.util.List<AuditLogEntity> findByAction(TypeActionAudit action) {
|
||||
return list("action = ?1 order by timestamp desc", action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recherche tous les logs d'audit pour un auteur donné.
|
||||
*
|
||||
* @param auteurAction Identifiant de l'auteur
|
||||
* @return Liste des logs triés par timestamp décroissant
|
||||
*/
|
||||
public static java.util.List<AuditLogEntity> findByAuteur(String auteurAction) {
|
||||
return list("auteurAction = ?1 order by timestamp desc", auteurAction);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recherche tous les logs d'audit dans une période donnée.
|
||||
*
|
||||
* @param startDate Date de début (inclusive)
|
||||
* @param endDate Date de fin (inclusive)
|
||||
* @return Liste des logs dans la période, triés par timestamp décroissant
|
||||
*/
|
||||
public static java.util.List<AuditLogEntity> findByPeriod(LocalDateTime startDate, LocalDateTime endDate) {
|
||||
return list("timestamp >= ?1 and timestamp <= ?2 order by timestamp desc", startDate, endDate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recherche tous les logs d'audit pour un realm donné.
|
||||
*
|
||||
* @param realmName Nom du realm
|
||||
* @return Liste des logs triés par timestamp décroissant
|
||||
*/
|
||||
public static java.util.List<AuditLogEntity> findByRealm(String realmName) {
|
||||
return list("realmName = ?1 order by timestamp desc", realmName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Supprime tous les logs d'audit plus anciens qu'une date donnée.
|
||||
* <p>Utile pour la maintenance et le respect des politiques de rétention.</p>
|
||||
*
|
||||
* @param beforeDate Date limite (les logs avant cette date seront supprimés)
|
||||
* @return Nombre de logs supprimés
|
||||
*/
|
||||
public static long deleteOlderThan(LocalDateTime beforeDate) {
|
||||
return delete("timestamp < ?1", beforeDate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compte le nombre d'actions effectuées par un auteur donné.
|
||||
*
|
||||
* @param auteurAction Identifiant de l'auteur
|
||||
* @return Nombre d'actions
|
||||
*/
|
||||
public static long countByAuteur(String auteurAction) {
|
||||
return count("auteurAction = ?1", auteurAction);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compte le nombre d'échecs pour un utilisateur donné.
|
||||
* <p>Utile pour détecter des problèmes récurrents.</p>
|
||||
*
|
||||
* @param userId ID de l'utilisateur
|
||||
* @return Nombre d'échecs
|
||||
*/
|
||||
public static long countFailuresByUserId(String userId) {
|
||||
return count("userId = ?1 and success = false", userId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package dev.lions.user.manager.server.impl.entity;
|
||||
|
||||
import io.quarkus.hibernate.orm.panache.PanacheEntity;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Table;
|
||||
import jakarta.persistence.Index;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* Entité représentant l'historique des synchronisations avec Keycloak.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "sync_history", indexes = {
|
||||
@Index(name = "idx_sync_realm", columnList = "realm_name"),
|
||||
@Index(name = "idx_sync_date", columnList = "sync_date")
|
||||
})
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class SyncHistoryEntity extends PanacheEntity {
|
||||
|
||||
@Column(name = "realm_name", nullable = false)
|
||||
private String realmName;
|
||||
|
||||
@Column(name = "sync_date", nullable = false)
|
||||
private LocalDateTime syncDate;
|
||||
|
||||
// USER ou ROLE
|
||||
@Column(name = "sync_type", nullable = false)
|
||||
private String syncType;
|
||||
|
||||
@Column(name = "status", nullable = false) // SUCCESS, FAILURE
|
||||
private String status;
|
||||
|
||||
@Column(name = "items_processed")
|
||||
private Integer itemsProcessed;
|
||||
|
||||
@Column(name = "duration_ms")
|
||||
private Long durationMs;
|
||||
|
||||
@Column(name = "error_message", columnDefinition = "TEXT")
|
||||
private String errorMessage;
|
||||
|
||||
public SyncHistoryEntity() {
|
||||
this.syncDate = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package dev.lions.user.manager.server.impl.entity;
|
||||
|
||||
import io.quarkus.hibernate.orm.panache.PanacheEntity;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Index;
|
||||
import jakarta.persistence.Table;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* Snapshot local d'un rôle Keycloak synchronisé.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "synced_role", indexes = {
|
||||
@Index(name = "idx_synced_role_realm", columnList = "realm_name"),
|
||||
@Index(name = "idx_synced_role_realm_name", columnList = "realm_name,role_name", unique = true)
|
||||
})
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class SyncedRoleEntity extends PanacheEntity {
|
||||
|
||||
@Column(name = "realm_name", nullable = false)
|
||||
private String realmName;
|
||||
|
||||
@Column(name = "role_name", nullable = false)
|
||||
private String roleName;
|
||||
|
||||
@Column(name = "description")
|
||||
private String description;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package dev.lions.user.manager.server.impl.entity;
|
||||
|
||||
import io.quarkus.hibernate.orm.panache.PanacheEntity;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Index;
|
||||
import jakarta.persistence.Table;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* Snapshot local d'un utilisateur Keycloak synchronisé.
|
||||
* Permet de conserver un état minimal pour des rapports ou vérifications de cohérence.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "synced_user", indexes = {
|
||||
@Index(name = "idx_synced_user_realm", columnList = "realm_name"),
|
||||
@Index(name = "idx_synced_user_realm_kc_id", columnList = "realm_name,keycloak_id", unique = true)
|
||||
})
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class SyncedUserEntity extends PanacheEntity {
|
||||
|
||||
@Column(name = "realm_name", nullable = false)
|
||||
private String realmName;
|
||||
|
||||
@Column(name = "keycloak_id", nullable = false)
|
||||
private String keycloakId;
|
||||
|
||||
@Column(name = "username", nullable = false)
|
||||
private String username;
|
||||
|
||||
@Column(name = "email")
|
||||
private String email;
|
||||
|
||||
@Column(name = "enabled")
|
||||
private Boolean enabled;
|
||||
|
||||
@Column(name = "email_verified")
|
||||
private Boolean emailVerified;
|
||||
|
||||
@Column(name = "created_at")
|
||||
private LocalDateTime createdAt;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
package dev.lions.user.manager.server.impl.interceptor;
|
||||
|
||||
import dev.lions.user.manager.dto.audit.AuditLogDTO;
|
||||
import dev.lions.user.manager.enums.audit.TypeActionAudit;
|
||||
import dev.lions.user.manager.service.AuditService;
|
||||
import io.quarkus.security.identity.SecurityIdentity;
|
||||
import jakarta.annotation.Priority;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.interceptor.AroundInvoke;
|
||||
import jakarta.interceptor.Interceptor;
|
||||
import jakarta.interceptor.InvocationContext;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Logged
|
||||
@Interceptor
|
||||
@Priority(Interceptor.Priority.APPLICATION)
|
||||
@Slf4j
|
||||
public class AuditInterceptor {
|
||||
|
||||
@Inject
|
||||
AuditService auditService;
|
||||
|
||||
@Inject
|
||||
SecurityIdentity securityIdentity;
|
||||
|
||||
@AroundInvoke
|
||||
public Object auditMethod(InvocationContext context) throws Exception {
|
||||
Logged annotation = context.getMethod().getAnnotation(Logged.class);
|
||||
if (annotation == null) {
|
||||
annotation = context.getTarget().getClass().getAnnotation(Logged.class);
|
||||
}
|
||||
|
||||
String actionStr = annotation != null ? annotation.action() : "UNKNOWN";
|
||||
String resourceType = annotation != null ? annotation.resource() : "UNKNOWN";
|
||||
String username = securityIdentity.isAnonymous() ? "anonymous" : securityIdentity.getPrincipal().getName();
|
||||
|
||||
// Extraction du realm depuis l'issuer JWT (ex: http://keycloak/realms/lions-user-manager)
|
||||
String realmName = "unknown";
|
||||
if (!securityIdentity.isAnonymous()
|
||||
&& securityIdentity.getPrincipal() instanceof org.eclipse.microprofile.jwt.JsonWebToken jwt) {
|
||||
String issuer = jwt.getIssuer();
|
||||
if (issuer != null && issuer.contains("/realms/")) {
|
||||
realmName = issuer.substring(issuer.lastIndexOf("/realms/") + 8);
|
||||
}
|
||||
}
|
||||
|
||||
// Tentative d'extraction de l'ID de la ressource (1er argument String)
|
||||
String resourceId = "";
|
||||
if (context.getParameters().length > 0 && context.getParameters()[0] instanceof String) {
|
||||
resourceId = (String) context.getParameters()[0];
|
||||
}
|
||||
|
||||
try {
|
||||
Object result = context.proceed();
|
||||
|
||||
// Log Success
|
||||
try {
|
||||
TypeActionAudit action = TypeActionAudit.valueOf(actionStr);
|
||||
auditService.logSuccess(
|
||||
action,
|
||||
resourceType,
|
||||
resourceId,
|
||||
null,
|
||||
realmName,
|
||||
username,
|
||||
"Action réussie via AOP");
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.warn("Type d'action audit inconnu: {}", actionStr);
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
// Log Failure
|
||||
try {
|
||||
TypeActionAudit action = TypeActionAudit.valueOf(actionStr);
|
||||
auditService.logFailure(
|
||||
action,
|
||||
resourceType,
|
||||
resourceId,
|
||||
null,
|
||||
realmName,
|
||||
username,
|
||||
"ERROR",
|
||||
e.getMessage());
|
||||
} catch (IllegalArgumentException ex) {
|
||||
log.warn("Type d'action audit inconnu: {}", actionStr);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package dev.lions.user.manager.server.impl.interceptor;
|
||||
|
||||
import jakarta.interceptor.InterceptorBinding;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* Annotation pour auditer automatiquement l'exécution d'une méthode.
|
||||
*/
|
||||
@InterceptorBinding
|
||||
@Target({ ElementType.METHOD, ElementType.TYPE })
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface Logged {
|
||||
|
||||
/**
|
||||
* Type d'action d'audit (ex: UPDATE_USER).
|
||||
*/
|
||||
String action() default "";
|
||||
|
||||
/**
|
||||
* Type de ressource concernée (ex: USER).
|
||||
*/
|
||||
String resource() default "";
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
package dev.lions.user.manager.server.impl.mapper;
|
||||
|
||||
import dev.lions.user.manager.dto.audit.AuditLogDTO;
|
||||
import dev.lions.user.manager.server.impl.entity.AuditLogEntity;
|
||||
import org.mapstruct.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Mapper MapStruct pour la conversion entre AuditLogEntity (JPA) et AuditLogDTO (API).
|
||||
*
|
||||
* <p>Ce mapper gère la transformation bidirectionnelle entre l'entité de persistance
|
||||
* et le DTO exposé via l'API REST, avec mapping automatique des champs compatibles.</p>
|
||||
*
|
||||
* <p><b>Fonctionnalités:</b></p>
|
||||
* <ul>
|
||||
* <li>Conversion Entity → DTO pour lecture/API</li>
|
||||
* <li>Conversion DTO → Entity pour persistance</li>
|
||||
* <li>Mapping de listes pour opérations bulk</li>
|
||||
* <li>Gestion automatique des types LocalDateTime</li>
|
||||
* <li>Mapping des enums (TypeActionAudit)</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p><b>Utilisation:</b></p>
|
||||
* <pre>
|
||||
* {@literal @}Inject
|
||||
* AuditLogMapper mapper;
|
||||
*
|
||||
* // Entity → DTO
|
||||
* AuditLogDTO dto = mapper.toDTO(entity);
|
||||
*
|
||||
* // DTO → Entity
|
||||
* AuditLogEntity entity = mapper.toEntity(dto);
|
||||
*
|
||||
* // Liste Entity → Liste DTO
|
||||
* List<AuditLogDTO> dtos = mapper.toDTOList(entities);
|
||||
* </pre>
|
||||
*
|
||||
* @see AuditLogEntity
|
||||
* @see AuditLogDTO
|
||||
* @author Lions Development Team
|
||||
* @version 1.0.0
|
||||
* @since 2026-01-02
|
||||
*/
|
||||
@Mapper(
|
||||
componentModel = MappingConstants.ComponentModel.JAKARTA_CDI,
|
||||
injectionStrategy = InjectionStrategy.CONSTRUCTOR,
|
||||
unmappedTargetPolicy = ReportingPolicy.IGNORE
|
||||
)
|
||||
public interface AuditLogMapper {
|
||||
|
||||
/**
|
||||
* Convertit une entité AuditLogEntity en DTO AuditLogDTO.
|
||||
*
|
||||
* <p>Mapping des champs Entity → DTO:</p>
|
||||
* <ul>
|
||||
* <li>id (Long) → id (String)</li>
|
||||
* <li>userId → ressourceId</li>
|
||||
* <li>action → typeAction</li>
|
||||
* <li>details → description</li>
|
||||
* <li>auteurAction → acteurUsername</li>
|
||||
* <li>timestamp → dateAction</li>
|
||||
* <li>ipAddress → ipAddress</li>
|
||||
* <li>userAgent → userAgent</li>
|
||||
* <li>realmName → realmName</li>
|
||||
* <li>success → success</li>
|
||||
* <li>errorMessage → errorMessage</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param entity L'entité JPA à convertir (peut être null)
|
||||
* @return Le DTO correspondant, ou null si l'entité est null
|
||||
*/
|
||||
@Mapping(target = "id", source = "id", qualifiedByName = "longToString")
|
||||
@Mapping(target = "ressourceId", source = "userId")
|
||||
@Mapping(target = "typeAction", source = "action")
|
||||
@Mapping(target = "description", source = "details")
|
||||
@Mapping(target = "acteurUsername", source = "auteurAction")
|
||||
@Mapping(target = "dateAction", source = "timestamp")
|
||||
AuditLogDTO toDTO(AuditLogEntity entity);
|
||||
|
||||
/**
|
||||
* Convertit un DTO AuditLogDTO en entité AuditLogEntity.
|
||||
*
|
||||
* <p>Utilisé pour créer une nouvelle entité à persister depuis les données API.</p>
|
||||
*
|
||||
* <p><b>Note:</b> L'ID de l'entité sera null (auto-généré par la DB),
|
||||
* même si l'ID du DTO est renseigné.</p>
|
||||
*
|
||||
* @param dto Le DTO à convertir (peut être null)
|
||||
* @return L'entité JPA correspondante, ou null si le DTO est null
|
||||
*/
|
||||
@Mapping(target = "id", ignore = true) // L'ID sera généré par la DB
|
||||
@Mapping(target = "userId", source = "ressourceId")
|
||||
@Mapping(target = "action", source = "typeAction")
|
||||
@Mapping(target = "details", source = "description")
|
||||
@Mapping(target = "auteurAction", source = "acteurUsername")
|
||||
@Mapping(target = "timestamp", source = "dateAction")
|
||||
AuditLogEntity toEntity(AuditLogDTO dto);
|
||||
|
||||
/**
|
||||
* Convertit une liste d'entités en liste de DTOs.
|
||||
*
|
||||
* <p>Utile pour les recherches qui retournent plusieurs résultats.</p>
|
||||
*
|
||||
* @param entities Liste des entités à convertir (peut être null ou vide)
|
||||
* @return Liste des DTOs correspondants, ou liste vide si entities est null/vide
|
||||
*/
|
||||
List<AuditLogDTO> toDTOList(List<AuditLogEntity> entities);
|
||||
|
||||
/**
|
||||
* Convertit une liste de DTOs en liste d'entités.
|
||||
*
|
||||
* <p>Utile pour les opérations d'import ou de création en masse.</p>
|
||||
*
|
||||
* @param dtos Liste des DTOs à convertir (peut être null ou vide)
|
||||
* @return Liste des entités correspondantes, ou liste vide si dtos est null/vide
|
||||
*/
|
||||
List<AuditLogEntity> toEntityList(List<AuditLogDTO> dtos);
|
||||
|
||||
/**
|
||||
* Met à jour une entité existante avec les données d'un DTO.
|
||||
*
|
||||
* <p>Préserve l'ID de l'entité et ne met à jour que les champs
|
||||
* présents dans le DTO.</p>
|
||||
*
|
||||
* <p><b>Utilisation:</b></p>
|
||||
* <pre>
|
||||
* AuditLogEntity existingEntity = AuditLogEntity.findById(id);
|
||||
* mapper.updateEntityFromDTO(dto, existingEntity);
|
||||
* existingEntity.persist();
|
||||
* </pre>
|
||||
*
|
||||
* @param dto Le DTO source contenant les nouvelles valeurs
|
||||
* @param entity L'entité cible à mettre à jour
|
||||
*/
|
||||
@Mapping(target = "id", ignore = true) // Préserve l'ID existant
|
||||
@Mapping(target = "userId", source = "ressourceId")
|
||||
@Mapping(target = "action", source = "typeAction")
|
||||
@Mapping(target = "details", source = "description")
|
||||
@Mapping(target = "auteurAction", source = "acteurUsername")
|
||||
@Mapping(target = "timestamp", source = "dateAction")
|
||||
@BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
|
||||
void updateEntityFromDTO(AuditLogDTO dto, @MappingTarget AuditLogEntity entity);
|
||||
|
||||
/**
|
||||
* Convertit un Long (ID de l'entité) en String (ID du DTO).
|
||||
*
|
||||
* <p>MapStruct appelle automatiquement cette méthode pour le mapping de l'ID.</p>
|
||||
*
|
||||
* @param id L'ID de type Long (peut être null)
|
||||
* @return L'ID converti en String, ou null si l'input est null
|
||||
*/
|
||||
@Named("longToString")
|
||||
default String longToString(Long id) {
|
||||
return id != null ? id.toString() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convertit un String (ID du DTO) en Long (ID de l'entité).
|
||||
*
|
||||
* <p>Utilisé lors de la conversion DTO → Entity si nécessaire.</p>
|
||||
*
|
||||
* @param id L'ID de type String (peut être null)
|
||||
* @return L'ID converti en Long, ou null si l'input est null ou invalide
|
||||
*/
|
||||
@Named("stringToLong")
|
||||
default Long stringToLong(String id) {
|
||||
if (id == null || id.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return Long.parseLong(id);
|
||||
} catch (NumberFormatException e) {
|
||||
// Log warning et retourne null en cas de format invalide
|
||||
System.err.println("WARN: Invalid ID format for conversion to Long: " + id);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package dev.lions.user.manager.server.impl.mapper;
|
||||
|
||||
import dev.lions.user.manager.dto.sync.SyncHistoryDTO;
|
||||
import dev.lions.user.manager.server.impl.entity.SyncHistoryEntity;
|
||||
import org.mapstruct.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Mapper(componentModel = MappingConstants.ComponentModel.JAKARTA_CDI, injectionStrategy = InjectionStrategy.CONSTRUCTOR, unmappedTargetPolicy = ReportingPolicy.IGNORE)
|
||||
public interface SyncHistoryMapper {
|
||||
|
||||
@Mapping(target = "id", source = "id", qualifiedByName = "longToString")
|
||||
SyncHistoryDTO toDTO(SyncHistoryEntity entity);
|
||||
|
||||
List<SyncHistoryDTO> toDTOList(List<SyncHistoryEntity> entities);
|
||||
|
||||
@Named("longToString")
|
||||
default String longToString(Long id) {
|
||||
return id != null ? id.toString() : null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package dev.lions.user.manager.server.impl.repository;
|
||||
|
||||
import dev.lions.user.manager.enums.audit.TypeActionAudit;
|
||||
import dev.lions.user.manager.server.impl.entity.AuditLogEntity;
|
||||
import io.quarkus.hibernate.orm.panache.PanacheRepository;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@ApplicationScoped
|
||||
public class AuditLogRepository implements PanacheRepository<AuditLogEntity> {
|
||||
|
||||
public List<AuditLogEntity> search(String realmName,
|
||||
String auteurAction,
|
||||
LocalDateTime dateDebut,
|
||||
LocalDateTime dateFin,
|
||||
String typeAction,
|
||||
Boolean success,
|
||||
int page,
|
||||
int pageSize) {
|
||||
|
||||
StringBuilder query = new StringBuilder("1=1");
|
||||
Map<String, Object> params = new HashMap<>();
|
||||
|
||||
// Construction dynamique de la requête
|
||||
if (realmName != null && !realmName.isEmpty()) {
|
||||
query.append(" AND realmName = :realmName");
|
||||
params.put("realmName", realmName);
|
||||
}
|
||||
if (auteurAction != null && !auteurAction.isEmpty()) {
|
||||
query.append(" AND auteurAction = :auteurAction");
|
||||
params.put("auteurAction", auteurAction);
|
||||
}
|
||||
if (dateDebut != null) {
|
||||
query.append(" AND timestamp >= :dateDebut");
|
||||
params.put("dateDebut", dateDebut);
|
||||
}
|
||||
if (dateFin != null) {
|
||||
query.append(" AND timestamp <= :dateFin");
|
||||
params.put("dateFin", dateFin);
|
||||
}
|
||||
if (typeAction != null && !typeAction.isEmpty()) {
|
||||
try {
|
||||
TypeActionAudit actionEnum = TypeActionAudit.valueOf(typeAction);
|
||||
query.append(" AND action = :actionEnum");
|
||||
params.put("actionEnum", actionEnum);
|
||||
} catch (IllegalArgumentException e) {
|
||||
// Ignore invalid enum value filter
|
||||
}
|
||||
}
|
||||
if (success != null) {
|
||||
query.append(" AND success = :success");
|
||||
params.put("success", success);
|
||||
}
|
||||
|
||||
query.append(" ORDER BY timestamp DESC");
|
||||
return find(query.toString(), params).page(page, pageSize).list();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package dev.lions.user.manager.server.impl.repository;
|
||||
|
||||
import dev.lions.user.manager.server.impl.entity.SyncHistoryEntity;
|
||||
import io.quarkus.hibernate.orm.panache.PanacheRepository;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@ApplicationScoped
|
||||
public class SyncHistoryRepository implements PanacheRepository<SyncHistoryEntity> {
|
||||
|
||||
public List<SyncHistoryEntity> findLatestByRealm(String realmName, int limit) {
|
||||
return find("realmName = ?1 ORDER BY syncDate DESC", realmName)
|
||||
.page(0, limit)
|
||||
.list();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package dev.lions.user.manager.server.impl.repository;
|
||||
|
||||
import dev.lions.user.manager.server.impl.entity.SyncedRoleEntity;
|
||||
import io.quarkus.hibernate.orm.panache.PanacheRepository;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@ApplicationScoped
|
||||
public class SyncedRoleRepository implements PanacheRepository<SyncedRoleEntity> {
|
||||
|
||||
/**
|
||||
* Remplace l'ensemble des snapshots de rôles pour un realm donné.
|
||||
*/
|
||||
public void replaceForRealm(String realmName, List<SyncedRoleEntity> roles) {
|
||||
delete("realmName", realmName);
|
||||
persist(roles);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package dev.lions.user.manager.server.impl.repository;
|
||||
|
||||
import dev.lions.user.manager.server.impl.entity.SyncedUserEntity;
|
||||
import io.quarkus.hibernate.orm.panache.PanacheRepository;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@ApplicationScoped
|
||||
public class SyncedUserRepository implements PanacheRepository<SyncedUserEntity> {
|
||||
|
||||
/**
|
||||
* Remplace l'ensemble des snapshots d'utilisateurs pour un realm donné.
|
||||
*/
|
||||
public void replaceForRealm(String realmName, List<SyncedUserEntity> users) {
|
||||
delete("realmName", realmName);
|
||||
persist(users);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,16 @@ package dev.lions.user.manager.service.impl;
|
||||
|
||||
import dev.lions.user.manager.dto.audit.AuditLogDTO;
|
||||
import dev.lions.user.manager.enums.audit.TypeActionAudit;
|
||||
// import dev.lions.user.manager.mapper.AuditLogMapper; // DELETE - Wrong package
|
||||
import dev.lions.user.manager.server.impl.mapper.AuditLogMapper; // ADD - Correct package
|
||||
import dev.lions.user.manager.server.impl.entity.AuditLogEntity;
|
||||
import dev.lions.user.manager.server.impl.repository.AuditLogRepository;
|
||||
import dev.lions.user.manager.service.AuditService;
|
||||
import io.quarkus.hibernate.orm.panache.PanacheQuery;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.transaction.Transactional;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
@@ -11,446 +19,344 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Implémentation du service d'audit
|
||||
*
|
||||
* NOTES:
|
||||
* - Cette implémentation utilise un stockage en mémoire pour le développement
|
||||
* - En production, il faudrait utiliser une base de données (PostgreSQL avec Panache)
|
||||
* - Les logs sont également écrits via SLF4J pour être capturés par les systèmes de logging centralisés
|
||||
*/
|
||||
@ApplicationScoped
|
||||
@Slf4j
|
||||
public class AuditServiceImpl implements AuditService {
|
||||
|
||||
// Stockage en mémoire (à remplacer par une DB en production)
|
||||
private final Map<String, AuditLogDTO> auditLogs = new ConcurrentHashMap<>();
|
||||
@Inject
|
||||
AuditLogRepository auditLogRepository;
|
||||
|
||||
@Inject
|
||||
AuditLogMapper auditLogMapper;
|
||||
|
||||
@Inject
|
||||
EntityManager entityManager;
|
||||
|
||||
@ConfigProperty(name = "lions.audit.enabled", defaultValue = "true")
|
||||
boolean auditEnabled;
|
||||
|
||||
@ConfigProperty(name = "lions.audit.log-to-database", defaultValue = "false")
|
||||
@ConfigProperty(name = "lions.audit.log-to-database", defaultValue = "true")
|
||||
boolean logToDatabase;
|
||||
|
||||
@Override
|
||||
@Transactional(Transactional.TxType.REQUIRES_NEW)
|
||||
public AuditLogDTO logAction(@Valid @NotNull AuditLogDTO auditLog) {
|
||||
if (!auditEnabled) {
|
||||
log.debug("Audit désactivé, log ignoré");
|
||||
log.debug("Audit désactivé, action ignorée: {}", auditLog.getTypeAction());
|
||||
return auditLog;
|
||||
}
|
||||
|
||||
// Générer un ID si nécessaire
|
||||
if (auditLog.getId() == null) {
|
||||
auditLog.setId(UUID.randomUUID().toString());
|
||||
log.info("AUDIT: [{}] {} - user:{} - ressource:{}/{} - status:{}",
|
||||
auditLog.getRealmName(),
|
||||
auditLog.getTypeAction(),
|
||||
auditLog.getActeurUsername(), // ou getActeurUserId()
|
||||
auditLog.getRessourceType(),
|
||||
auditLog.getRessourceId(),
|
||||
auditLog.getSuccess() != null && auditLog.getSuccess() ? "SUCCESS" : "FAILURE");
|
||||
|
||||
if (logToDatabase) {
|
||||
try {
|
||||
// Ensure dateAction is set
|
||||
if (auditLog.getDateAction() == null) {
|
||||
auditLog.setDateAction(LocalDateTime.now());
|
||||
}
|
||||
|
||||
AuditLogEntity entity = auditLogMapper.toEntity(auditLog);
|
||||
auditLogRepository.persist(entity);
|
||||
|
||||
// Mettre à jour l'ID du DTO avec l'ID généré par la base
|
||||
if (entity.id != null) {
|
||||
auditLog.setId(entity.id.toString());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors de la persistance du log d'audit", e);
|
||||
// On ne bloque pas l'action métier si l'audit échoue (sauf exigence contraire)
|
||||
}
|
||||
}
|
||||
|
||||
// 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.isSuccessful(),
|
||||
auditLog.getIpAddress(),
|
||||
auditLog.getDescription());
|
||||
|
||||
// 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
|
||||
@Transactional(Transactional.TxType.REQUIRES_NEW)
|
||||
public void logSuccess(@NotNull TypeActionAudit typeAction,
|
||||
@NotBlank String ressourceType,
|
||||
String ressourceId,
|
||||
String ressourceName,
|
||||
@NotBlank String realmName,
|
||||
@NotBlank String acteurUserId,
|
||||
String description) {
|
||||
AuditLogDTO auditLog = AuditLogDTO.builder()
|
||||
.acteurUserId(acteurUserId)
|
||||
.acteurUsername(acteurUserId) // Utiliser acteurUserId comme username pour l'instant
|
||||
.typeAction(typeAction)
|
||||
.ressourceType(ressourceType)
|
||||
.ressourceId(ressourceId != null ? ressourceId : "")
|
||||
.success(true)
|
||||
.description(description)
|
||||
.dateAction(LocalDateTime.now())
|
||||
.build();
|
||||
@NotBlank String ressourceType,
|
||||
String ressourceId,
|
||||
String ressourceName,
|
||||
@NotBlank String realmName,
|
||||
@NotBlank String acteurUserId,
|
||||
String description) {
|
||||
|
||||
logAction(auditLog);
|
||||
AuditLogDTO log = AuditLogDTO.builder()
|
||||
.typeAction(typeAction)
|
||||
.ressourceType(ressourceType)
|
||||
.ressourceId(ressourceId)
|
||||
.ressourceName(ressourceName)
|
||||
.realmName(realmName)
|
||||
.acteurUserId(acteurUserId)
|
||||
.acteurUsername(acteurUserId) // On map aussi le username pour la persistence Entity
|
||||
.description(description)
|
||||
.dateAction(LocalDateTime.now())
|
||||
.success(true)
|
||||
.build();
|
||||
|
||||
logAction(log);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(Transactional.TxType.REQUIRES_NEW)
|
||||
public void logFailure(@NotNull TypeActionAudit typeAction,
|
||||
@NotBlank String ressourceType,
|
||||
String ressourceId,
|
||||
String ressourceName,
|
||||
@NotBlank String realmName,
|
||||
@NotBlank String acteurUserId,
|
||||
String errorCode,
|
||||
String errorMessage) {
|
||||
AuditLogDTO auditLog = AuditLogDTO.builder()
|
||||
.acteurUserId(acteurUserId)
|
||||
.acteurUsername(acteurUserId) // Utiliser acteurUserId comme username pour l'instant
|
||||
.typeAction(typeAction)
|
||||
.ressourceType(ressourceType)
|
||||
.ressourceId(ressourceId != null ? ressourceId : "")
|
||||
.success(false)
|
||||
.errorMessage(errorMessage)
|
||||
.dateAction(LocalDateTime.now())
|
||||
.build();
|
||||
@NotBlank String ressourceType,
|
||||
String ressourceId,
|
||||
String ressourceName,
|
||||
@NotBlank String realmName,
|
||||
@NotBlank String acteurUserId,
|
||||
String errorCode,
|
||||
String errorMessage) {
|
||||
|
||||
logAction(auditLog);
|
||||
AuditLogDTO log = AuditLogDTO.builder()
|
||||
.typeAction(typeAction)
|
||||
.ressourceType(ressourceType)
|
||||
.ressourceId(ressourceId)
|
||||
.ressourceName(ressourceName)
|
||||
.realmName(realmName)
|
||||
.acteurUserId(acteurUserId)
|
||||
.acteurUsername(acteurUserId)
|
||||
.description("Echec: " + errorCode)
|
||||
.errorMessage(errorMessage)
|
||||
.dateAction(LocalDateTime.now())
|
||||
.success(false)
|
||||
.build();
|
||||
|
||||
logAction(log);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AuditLogDTO> findByActeur(@NotBlank String acteurUserId,
|
||||
LocalDateTime dateDebut,
|
||||
LocalDateTime dateFin,
|
||||
int page,
|
||||
int pageSize) {
|
||||
return searchLogs(acteurUserId, dateDebut, dateFin, null, null, null, page, pageSize);
|
||||
LocalDateTime dateDebut,
|
||||
LocalDateTime dateFin,
|
||||
int page,
|
||||
int pageSize) {
|
||||
// Le repository cherche par auteurAction, qui est mappé sur acteurUsername dans
|
||||
// le DTO
|
||||
List<AuditLogEntity> entities = auditLogRepository.search(null, acteurUserId, dateDebut, dateFin, null, null,
|
||||
page,
|
||||
pageSize);
|
||||
return auditLogMapper.toDTOList(entities);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AuditLogDTO> findByRessource(@NotBlank String ressourceType,
|
||||
@NotBlank String ressourceId,
|
||||
LocalDateTime dateDebut,
|
||||
LocalDateTime dateFin,
|
||||
int page,
|
||||
int pageSize) {
|
||||
return searchLogs(null, dateDebut, dateFin, null, ressourceType, null, page, pageSize)
|
||||
.stream()
|
||||
.filter(log -> ressourceId.equals(log.getRessourceId()))
|
||||
.collect(Collectors.toList());
|
||||
@NotBlank String ressourceId,
|
||||
LocalDateTime dateDebut,
|
||||
LocalDateTime dateFin,
|
||||
int page,
|
||||
int pageSize) {
|
||||
|
||||
// Utilisation de Panache query directe car le repo search générique est limité
|
||||
// On cherche dans 'details' (description) ou 'userId' (ressourceId)
|
||||
String filter = "%" + ressourceId + "%";
|
||||
// Correction: userId est le nom du champ dans l'entité qui mappe ressourceId
|
||||
PanacheQuery<AuditLogEntity> q = auditLogRepository.find("userId = ?1 or details like ?2", ressourceId, filter);
|
||||
|
||||
return auditLogMapper.toDTOList(q.page(page, pageSize).list());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AuditLogDTO> findByTypeAction(@NotNull TypeActionAudit typeAction,
|
||||
@NotBlank String realmName,
|
||||
LocalDateTime dateDebut,
|
||||
LocalDateTime dateFin,
|
||||
int page,
|
||||
int pageSize) {
|
||||
return searchLogs(null, dateDebut, dateFin, typeAction, null, null, page, pageSize);
|
||||
@NotBlank String realmName,
|
||||
LocalDateTime dateDebut,
|
||||
LocalDateTime dateFin,
|
||||
int page,
|
||||
int pageSize) {
|
||||
List<AuditLogEntity> entities = auditLogRepository.search(realmName, null, dateDebut, dateFin,
|
||||
typeAction.name(), null, page,
|
||||
pageSize);
|
||||
return auditLogMapper.toDTOList(entities);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AuditLogDTO> findByRealm(@NotBlank String realmName,
|
||||
LocalDateTime dateDebut,
|
||||
LocalDateTime dateFin,
|
||||
int page,
|
||||
int pageSize) {
|
||||
// Pour l'instant, on retourne tous les logs car on n'a pas de champ realmName dans AuditLogDTO
|
||||
return searchLogs(null, dateDebut, dateFin, null, null, null, page, pageSize);
|
||||
LocalDateTime dateDebut,
|
||||
LocalDateTime dateFin,
|
||||
int page,
|
||||
int pageSize) {
|
||||
List<AuditLogEntity> entities = auditLogRepository.search(realmName, null, dateDebut, dateFin, null, null, page,
|
||||
pageSize);
|
||||
return auditLogMapper.toDTOList(entities);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AuditLogDTO> findFailures(@NotBlank String realmName,
|
||||
LocalDateTime dateDebut,
|
||||
LocalDateTime dateFin,
|
||||
int page,
|
||||
int pageSize) {
|
||||
return searchLogs(null, dateDebut, dateFin, null, null, false, page, pageSize);
|
||||
LocalDateTime dateDebut,
|
||||
LocalDateTime dateFin,
|
||||
int page,
|
||||
int pageSize) {
|
||||
List<AuditLogEntity> entities = auditLogRepository.search(realmName, null, dateDebut, dateFin, null, false,
|
||||
page,
|
||||
pageSize);
|
||||
return auditLogMapper.toDTOList(entities);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AuditLogDTO> findCriticalActions(@NotBlank String realmName,
|
||||
LocalDateTime dateDebut,
|
||||
LocalDateTime dateFin,
|
||||
int page,
|
||||
int pageSize) {
|
||||
// Les actions critiques sont USER_DELETE, ROLE_DELETE, etc.
|
||||
return auditLogs.values().stream()
|
||||
.filter(log -> {
|
||||
TypeActionAudit type = log.getTypeAction();
|
||||
return type == TypeActionAudit.USER_DELETE ||
|
||||
type == TypeActionAudit.ROLE_DELETE ||
|
||||
type == TypeActionAudit.SESSION_REVOKE_ALL;
|
||||
})
|
||||
.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) -> b.getDateAction().compareTo(a.getDateAction()))
|
||||
.skip((long) page * pageSize)
|
||||
.limit(pageSize)
|
||||
.collect(Collectors.toList());
|
||||
LocalDateTime dateDebut,
|
||||
LocalDateTime dateFin,
|
||||
int page,
|
||||
int pageSize) {
|
||||
List<AuditLogEntity> entities = auditLogRepository.search(realmName, null, dateDebut, dateFin, null, false,
|
||||
page, pageSize);
|
||||
return auditLogMapper.toDTOList(entities);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public Map<TypeActionAudit, Long> countByActionType(@NotBlank String realmName,
|
||||
LocalDateTime dateDebut,
|
||||
LocalDateTime dateFin) {
|
||||
return getActionStatistics(dateDebut, dateFin);
|
||||
LocalDateTime dateDebut,
|
||||
LocalDateTime dateFin) {
|
||||
StringBuilder sql = new StringBuilder("SELECT action, COUNT(*) AS cnt FROM audit_logs WHERE realm_name = :realmName");
|
||||
if (dateDebut != null) sql.append(" AND timestamp >= :dateDebut");
|
||||
if (dateFin != null) sql.append(" AND timestamp <= :dateFin");
|
||||
sql.append(" GROUP BY action");
|
||||
var query = entityManager.createNativeQuery(sql.toString())
|
||||
.setParameter("realmName", realmName);
|
||||
if (dateDebut != null) query.setParameter("dateDebut", dateDebut);
|
||||
if (dateFin != null) query.setParameter("dateFin", dateFin);
|
||||
List<Object[]> rows = query.getResultList();
|
||||
Map<TypeActionAudit, Long> result = new HashMap<>();
|
||||
for (Object[] row : rows) {
|
||||
String actionStr = (String) row[0];
|
||||
Long count = ((Number) row[1]).longValue();
|
||||
try {
|
||||
result.put(TypeActionAudit.valueOf(actionStr), count);
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.debug("TypeActionAudit inconnu ignoré: {}", actionStr);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public Map<String, Long> countByActeur(@NotBlank String realmName,
|
||||
LocalDateTime dateDebut,
|
||||
LocalDateTime dateFin) {
|
||||
return getUserActivityStatistics(dateDebut, dateFin);
|
||||
LocalDateTime dateDebut,
|
||||
LocalDateTime dateFin) {
|
||||
StringBuilder sql = new StringBuilder("SELECT auteur_action, COUNT(*) AS cnt FROM audit_logs WHERE realm_name = :realmName");
|
||||
if (dateDebut != null) sql.append(" AND timestamp >= :dateDebut");
|
||||
if (dateFin != null) sql.append(" AND timestamp <= :dateFin");
|
||||
sql.append(" GROUP BY auteur_action ORDER BY COUNT(*) DESC LIMIT 10");
|
||||
var query = entityManager.createNativeQuery(sql.toString())
|
||||
.setParameter("realmName", realmName);
|
||||
if (dateDebut != null) query.setParameter("dateDebut", dateDebut);
|
||||
if (dateFin != null) query.setParameter("dateFin", dateFin);
|
||||
List<Object[]> rows = query.getResultList();
|
||||
Map<String, Long> result = new HashMap<>();
|
||||
for (Object[] row : rows) {
|
||||
result.put((String) row[0], ((Number) row[1]).longValue());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public Map<String, Long> countSuccessVsFailure(@NotBlank String realmName,
|
||||
LocalDateTime dateDebut,
|
||||
LocalDateTime dateFin) {
|
||||
long successCount = getSuccessCount(dateDebut, dateFin);
|
||||
long failureCount = getFailureCount(dateDebut, dateFin);
|
||||
|
||||
Map<String, Long> result = new java.util.HashMap<>();
|
||||
result.put("success", successCount);
|
||||
result.put("failure", failureCount);
|
||||
LocalDateTime dateDebut,
|
||||
LocalDateTime dateFin) {
|
||||
StringBuilder sql = new StringBuilder("SELECT success, COUNT(*) AS cnt FROM audit_logs WHERE realm_name = :realmName");
|
||||
if (dateDebut != null) sql.append(" AND timestamp >= :dateDebut");
|
||||
if (dateFin != null) sql.append(" AND timestamp <= :dateFin");
|
||||
sql.append(" GROUP BY success");
|
||||
var query = entityManager.createNativeQuery(sql.toString())
|
||||
.setParameter("realmName", realmName);
|
||||
if (dateDebut != null) query.setParameter("dateDebut", dateDebut);
|
||||
if (dateFin != null) query.setParameter("dateFin", dateFin);
|
||||
List<Object[]> rows = query.getResultList();
|
||||
Map<String, Long> result = new HashMap<>();
|
||||
result.put("success", 0L);
|
||||
result.put("failure", 0L);
|
||||
for (Object[] row : rows) {
|
||||
Boolean success = (Boolean) row[0];
|
||||
Long count = ((Number) row[1]).longValue();
|
||||
result.put(Boolean.TRUE.equals(success) ? "success" : "failure", count);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String exportToCSV(@NotBlank String realmName,
|
||||
LocalDateTime dateDebut,
|
||||
LocalDateTime dateFin) {
|
||||
List<String> csvLines = exportLogsToCSV(dateDebut, dateFin);
|
||||
return String.join("\n", csvLines);
|
||||
LocalDateTime dateDebut,
|
||||
LocalDateTime dateFin) {
|
||||
List<AuditLogEntity> entities = auditLogRepository.search(realmName, null, dateDebut, dateFin, null, null, 0, Integer.MAX_VALUE);
|
||||
List<AuditLogDTO> logs = auditLogMapper.toDTOList(entities);
|
||||
StringBuilder csv = new StringBuilder();
|
||||
csv.append("id;typeAction;acteur;realmName;ressourceType;ressourceId;succes;dateAction;message\n");
|
||||
for (AuditLogDTO dto : logs) {
|
||||
csv.append(escapeCsv(dto.getId()));
|
||||
csv.append(";");
|
||||
csv.append(escapeCsv(dto.getTypeAction() != null ? dto.getTypeAction().name() : ""));
|
||||
csv.append(";");
|
||||
csv.append(escapeCsv(dto.getActeurUsername()));
|
||||
csv.append(";");
|
||||
csv.append(escapeCsv(dto.getRealmName()));
|
||||
csv.append(";");
|
||||
csv.append(escapeCsv(dto.getRessourceType()));
|
||||
csv.append(";");
|
||||
csv.append(escapeCsv(dto.getRessourceId()));
|
||||
csv.append(";");
|
||||
csv.append(dto.getSuccess() != null && dto.getSuccess() ? "true" : "false");
|
||||
csv.append(";");
|
||||
csv.append(dto.getDateAction() != null ? dto.getDateAction().toString() : "");
|
||||
csv.append(";");
|
||||
csv.append(escapeCsv(dto.getErrorMessage() != null ? dto.getErrorMessage() : (dto.getDescription() != null ? dto.getDescription() : "")));
|
||||
csv.append("\n");
|
||||
}
|
||||
return csv.toString();
|
||||
}
|
||||
|
||||
private static String escapeCsv(String value) {
|
||||
if (value == null) return "";
|
||||
if (value.contains(";") || value.contains("\"") || value.contains("\n")) {
|
||||
return "\"" + value.replace("\"", "\"\"") + "\"";
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public long purgeOldLogs(@NotNull LocalDateTime dateLimite) {
|
||||
long beforeCount = auditLogs.size();
|
||||
auditLogs.entrySet().removeIf(entry ->
|
||||
entry.getValue().getDateAction().isBefore(dateLimite)
|
||||
);
|
||||
long afterCount = auditLogs.size();
|
||||
return beforeCount - afterCount;
|
||||
return auditLogRepository.delete("timestamp < ?1", dateLimite);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getAuditStatistics(@NotBlank String realmName,
|
||||
LocalDateTime dateDebut,
|
||||
LocalDateTime dateFin) {
|
||||
LocalDateTime dateDebut,
|
||||
LocalDateTime dateFin) {
|
||||
Map<String, Object> stats = new java.util.HashMap<>();
|
||||
stats.put("total", 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;
|
||||
})
|
||||
.count());
|
||||
stats.put("success", getSuccessCount(dateDebut, dateFin));
|
||||
stats.put("failure", getFailureCount(dateDebut, dateFin));
|
||||
stats.put("byActionType", countByActionType(realmName, dateDebut, dateFin));
|
||||
stats.put("byActeur", countByActeur(realmName, dateDebut, dateFin));
|
||||
stats.put("total", auditLogRepository.count("realmName", realmName));
|
||||
return stats;
|
||||
}
|
||||
|
||||
// Méthode privée helper pour la recherche
|
||||
private List<AuditLogDTO> searchLogs(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.isSuccessful()) {
|
||||
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());
|
||||
}
|
||||
|
||||
// Méthodes privées helper
|
||||
private Map<TypeActionAudit, Long> getActionStatistics(LocalDateTime dateDebut, LocalDateTime dateFin) {
|
||||
log.debug("Calcul des statistiques d'actions entre {} et {}", dateDebut, dateFin);
|
||||
|
||||
return auditLogs.values().stream()
|
||||
.filter(log -> {
|
||||
if (dateDebut != null && log.getDateAction().isBefore(dateDebut)) {
|
||||
return false;
|
||||
}
|
||||
if (dateFin != null && log.getDateAction().isAfter(dateFin)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.collect(Collectors.groupingBy(
|
||||
AuditLogDTO::getTypeAction,
|
||||
Collectors.counting()
|
||||
));
|
||||
}
|
||||
|
||||
private Map<String, Long> getUserActivityStatistics(LocalDateTime dateDebut, LocalDateTime dateFin) {
|
||||
log.debug("Calcul des statistiques d'activité utilisateurs entre {} et {}", dateDebut, dateFin);
|
||||
|
||||
return auditLogs.values().stream()
|
||||
.filter(log -> {
|
||||
if (dateDebut != null && log.getDateAction().isBefore(dateDebut)) {
|
||||
return false;
|
||||
}
|
||||
if (dateFin != null && log.getDateAction().isAfter(dateFin)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.collect(Collectors.groupingBy(
|
||||
AuditLogDTO::getActeurUsername,
|
||||
Collectors.counting()
|
||||
));
|
||||
}
|
||||
|
||||
private long getFailureCount(LocalDateTime dateDebut, LocalDateTime dateFin) {
|
||||
log.debug("Comptage des échecs entre {} et {}", dateDebut, dateFin);
|
||||
|
||||
return auditLogs.values().stream()
|
||||
.filter(log -> {
|
||||
if (log.isSuccessful()) {
|
||||
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();
|
||||
}
|
||||
|
||||
private long getSuccessCount(LocalDateTime dateDebut, LocalDateTime dateFin) {
|
||||
log.debug("Comptage des succès entre {} et {}", dateDebut, dateFin);
|
||||
|
||||
return auditLogs.values().stream()
|
||||
.filter(log -> {
|
||||
if (!log.isSuccessful()) {
|
||||
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();
|
||||
}
|
||||
|
||||
private List<String> exportLogsToCSV(LocalDateTime dateDebut, LocalDateTime dateFin) {
|
||||
log.info("Export CSV des logs d'audit entre {} et {}", dateDebut, dateFin);
|
||||
|
||||
List<String> csvLines = new ArrayList<>();
|
||||
|
||||
// En-tête CSV
|
||||
csvLines.add("ID,Date Action,Acteur,Type Action,Ressource Type,Ressource ID,Succès,Adresse IP,Détails,Message Erreur");
|
||||
|
||||
// Données
|
||||
auditLogs.values().stream()
|
||||
.filter(log -> {
|
||||
if (dateDebut != null && log.getDateAction().isBefore(dateDebut)) {
|
||||
return false;
|
||||
}
|
||||
if (dateFin != null && log.getDateAction().isAfter(dateFin)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.sorted((a, b) -> a.getDateAction().compareTo(b.getDateAction()))
|
||||
.forEach(log -> {
|
||||
String csvLine = String.format("%s,%s,%s,%s,%s,%s,%s,%s,\"%s\",\"%s\"",
|
||||
log.getId(),
|
||||
log.getDateAction(),
|
||||
log.getActeurUsername(),
|
||||
log.getTypeAction(),
|
||||
log.getRessourceType(),
|
||||
log.getRessourceId(),
|
||||
log.isSuccessful(),
|
||||
log.getIpAddress() != null ? log.getIpAddress() : "",
|
||||
log.getDescription() != null ? log.getDescription().replace("\"", "\"\"") : "",
|
||||
log.getErrorMessage() != null ? log.getErrorMessage().replace("\"", "\"\"") : ""
|
||||
);
|
||||
csvLines.add(csvLine);
|
||||
});
|
||||
|
||||
log.info("Export CSV terminé: {} lignes", csvLines.size() - 1);
|
||||
return csvLines;
|
||||
}
|
||||
|
||||
// ==================== Méthodes utilitaires ====================
|
||||
|
||||
/**
|
||||
* Retourne le nombre total de logs en mémoire
|
||||
* Retourne le nombre total de logs (Utilisé par les tests)
|
||||
*/
|
||||
public long getTotalCount() {
|
||||
return auditLogs.size();
|
||||
return auditLogRepository.count();
|
||||
}
|
||||
|
||||
/**
|
||||
* Vide tous les logs (ATTENTION: à utiliser uniquement en développement)
|
||||
* Vide tous les logs (Utilisé par les tests)
|
||||
*/
|
||||
@Transactional
|
||||
public void clearAll() {
|
||||
log.warn("ATTENTION: Suppression de tous les logs d'audit en mémoire");
|
||||
auditLogs.clear();
|
||||
log.warn("ATTENTION: Suppression de tous les logs d'audit en base");
|
||||
auditLogRepository.deleteAll();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
package dev.lions.user.manager.service.impl;
|
||||
|
||||
import lombok.experimental.UtilityClass;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Classe utilitaire pour la validation des données CSV lors de l'import d'utilisateurs
|
||||
*
|
||||
* @author Lions Development Team
|
||||
* @version 1.0.0
|
||||
* @since 2026-01-02
|
||||
*/
|
||||
@Slf4j
|
||||
@UtilityClass
|
||||
public class CsvValidationHelper {
|
||||
|
||||
/**
|
||||
* Pattern pour valider le format d'email selon RFC 5322 (simplifié)
|
||||
*/
|
||||
private static final Pattern EMAIL_PATTERN = Pattern.compile(
|
||||
"^[a-zA-Z0-9_+&*-]+(?:\\.[a-zA-Z0-9_+&*-]+)*@(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,7}$"
|
||||
);
|
||||
|
||||
/**
|
||||
* Pattern pour valider le username (alphanumérique, tirets, underscores, points)
|
||||
*/
|
||||
private static final Pattern USERNAME_PATTERN = Pattern.compile(
|
||||
"^[a-zA-Z0-9._-]{2,255}$"
|
||||
);
|
||||
|
||||
/**
|
||||
* Longueur minimale pour un username
|
||||
*/
|
||||
private static final int USERNAME_MIN_LENGTH = 2;
|
||||
|
||||
/**
|
||||
* Longueur maximale pour un username
|
||||
*/
|
||||
private static final int USERNAME_MAX_LENGTH = 255;
|
||||
|
||||
/**
|
||||
* Longueur maximale pour un nom ou prénom
|
||||
*/
|
||||
private static final int NAME_MAX_LENGTH = 255;
|
||||
|
||||
/**
|
||||
* Valide le format d'un email
|
||||
*
|
||||
* @param email Email à valider
|
||||
* @return true si l'email est valide, false sinon
|
||||
*/
|
||||
public static boolean isValidEmail(String email) {
|
||||
if (email == null || email.isBlank()) {
|
||||
return false;
|
||||
}
|
||||
return EMAIL_PATTERN.matcher(email.trim()).matches();
|
||||
}
|
||||
|
||||
/**
|
||||
* Valide un username
|
||||
*
|
||||
* @param username Username à valider
|
||||
* @return Message d'erreur si invalide, null si valide
|
||||
*/
|
||||
public static String validateUsername(String username) {
|
||||
if (username == null || username.isBlank()) {
|
||||
return "Username obligatoire";
|
||||
}
|
||||
|
||||
String trimmed = username.trim();
|
||||
|
||||
if (trimmed.length() < USERNAME_MIN_LENGTH) {
|
||||
return String.format("Username trop court (minimum %d caractères)", USERNAME_MIN_LENGTH);
|
||||
}
|
||||
|
||||
if (trimmed.length() > USERNAME_MAX_LENGTH) {
|
||||
return String.format("Username trop long (maximum %d caractères)", USERNAME_MAX_LENGTH);
|
||||
}
|
||||
|
||||
if (!USERNAME_PATTERN.matcher(trimmed).matches()) {
|
||||
return "Username invalide (autorisé: lettres, chiffres, .-_)";
|
||||
}
|
||||
|
||||
return null; // Valide
|
||||
}
|
||||
|
||||
/**
|
||||
* Valide un email (peut être vide)
|
||||
*
|
||||
* @param email Email à valider
|
||||
* @return Message d'erreur si invalide, null si valide ou vide
|
||||
*/
|
||||
public static String validateEmail(String email) {
|
||||
if (email == null || email.isBlank()) {
|
||||
return null; // Email optionnel
|
||||
}
|
||||
|
||||
if (!isValidEmail(email)) {
|
||||
return "Format d'email invalide";
|
||||
}
|
||||
|
||||
return null; // Valide
|
||||
}
|
||||
|
||||
/**
|
||||
* Valide un nom ou prénom
|
||||
*
|
||||
* @param name Nom à valider
|
||||
* @param fieldName Nom du champ pour les messages d'erreur
|
||||
* @return Message d'erreur si invalide, null si valide
|
||||
*/
|
||||
public static String validateName(String name, String fieldName) {
|
||||
if (name == null || name.isBlank()) {
|
||||
return null; // Nom optionnel
|
||||
}
|
||||
|
||||
String trimmed = name.trim();
|
||||
|
||||
if (trimmed.length() > NAME_MAX_LENGTH) {
|
||||
return String.format("%s trop long (maximum %d caractères)", fieldName, NAME_MAX_LENGTH);
|
||||
}
|
||||
|
||||
return null; // Valide
|
||||
}
|
||||
|
||||
/**
|
||||
* Valide une valeur boolean
|
||||
*
|
||||
* @param value Valeur à valider
|
||||
* @return Message d'erreur si invalide, null si valide
|
||||
*/
|
||||
public static String validateBoolean(String value) {
|
||||
if (value == null || value.isBlank()) {
|
||||
return null; // Optionnel, défaut à false
|
||||
}
|
||||
|
||||
String trimmed = value.trim().toLowerCase();
|
||||
if (!trimmed.equals("true") && !trimmed.equals("false") &&
|
||||
!trimmed.equals("1") && !trimmed.equals("0") &&
|
||||
!trimmed.equals("yes") && !trimmed.equals("no")) {
|
||||
return "Valeur boolean invalide (attendu: true/false, 1/0, yes/no)";
|
||||
}
|
||||
|
||||
return null; // Valide
|
||||
}
|
||||
|
||||
/**
|
||||
* Convertit une chaîne en boolean
|
||||
*
|
||||
* @param value Valeur à convertir
|
||||
* @return boolean correspondant
|
||||
*/
|
||||
public static boolean parseBoolean(String value) {
|
||||
if (value == null || value.isBlank()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String trimmed = value.trim().toLowerCase();
|
||||
return trimmed.equals("true") || trimmed.equals("1") || trimmed.equals("yes");
|
||||
}
|
||||
|
||||
/**
|
||||
* Nettoie une chaîne (trim et null si vide)
|
||||
*
|
||||
* @param value Valeur à nettoyer
|
||||
* @return Valeur nettoyée ou null
|
||||
*/
|
||||
public static String clean(String value) {
|
||||
if (value == null || value.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
return value.trim();
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,216 +1,389 @@
|
||||
package dev.lions.user.manager.service.impl;
|
||||
|
||||
import dev.lions.user.manager.client.KeycloakAdminClient;
|
||||
import dev.lions.user.manager.dto.role.RoleDTO;
|
||||
import dev.lions.user.manager.dto.sync.HealthStatusDTO;
|
||||
import dev.lions.user.manager.dto.sync.SyncResultDTO;
|
||||
import dev.lions.user.manager.dto.user.UserDTO;
|
||||
import dev.lions.user.manager.enums.role.TypeRole;
|
||||
import dev.lions.user.manager.mapper.RoleMapper;
|
||||
import dev.lions.user.manager.mapper.UserMapper;
|
||||
import dev.lions.user.manager.server.impl.entity.SyncHistoryEntity;
|
||||
import dev.lions.user.manager.server.impl.entity.SyncedRoleEntity;
|
||||
import dev.lions.user.manager.server.impl.entity.SyncedUserEntity;
|
||||
import dev.lions.user.manager.server.impl.interceptor.Logged;
|
||||
import dev.lions.user.manager.server.impl.repository.SyncHistoryRepository;
|
||||
import dev.lions.user.manager.server.impl.repository.SyncedRoleRepository;
|
||||
import dev.lions.user.manager.server.impl.repository.SyncedUserRepository;
|
||||
import dev.lions.user.manager.service.SyncService;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.transaction.Transactional;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import dev.lions.user.manager.client.KeycloakAdminClient;
|
||||
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||
import org.keycloak.admin.client.Keycloak;
|
||||
import org.keycloak.representations.idm.RoleRepresentation;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Implémentation du service de synchronisation avec Keycloak
|
||||
*
|
||||
* Ce service permet de:
|
||||
* - Synchroniser les utilisateurs depuis Keycloak
|
||||
* - Synchroniser les rôles depuis Keycloak
|
||||
* - Vérifier la cohérence des données
|
||||
* - Effectuer des health checks sur Keycloak
|
||||
*/
|
||||
@ApplicationScoped
|
||||
@Slf4j
|
||||
public class SyncServiceImpl implements SyncService {
|
||||
|
||||
@Inject
|
||||
Keycloak keycloak;
|
||||
|
||||
@Inject
|
||||
KeycloakAdminClient keycloakAdminClient;
|
||||
|
||||
@Inject
|
||||
SyncHistoryRepository syncHistoryRepository;
|
||||
|
||||
// Repositories optionnels pour la persistance locale des snapshots.
|
||||
// Ils sont marqués @Inject mais l'utilisation dans le code est protégée
|
||||
// par des checks null pour ne pas casser les tests existants.
|
||||
@Inject
|
||||
SyncedUserRepository syncedUserRepository;
|
||||
|
||||
@Inject
|
||||
SyncedRoleRepository syncedRoleRepository;
|
||||
|
||||
@ConfigProperty(name = "lions.keycloak.server-url")
|
||||
String keycloakServerUrl;
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Logged(action = "SYNC_USERS", resource = "REALM")
|
||||
public int syncUsersFromRealm(@NotBlank String realmName) {
|
||||
log.info("Synchronisation des utilisateurs depuis le realm: {}", realmName);
|
||||
LocalDateTime start = LocalDateTime.now();
|
||||
int count = 0;
|
||||
String status = "SUCCESS";
|
||||
String errorMessage = null;
|
||||
|
||||
try {
|
||||
List<UserRepresentation> userReps = keycloakAdminClient.getInstance()
|
||||
.realm(realmName)
|
||||
.users()
|
||||
.list();
|
||||
List<UserRepresentation> users = keycloak.realm(realmName).users().list();
|
||||
count = users.size();
|
||||
|
||||
// Persister un snapshot minimal des utilisateurs dans la base locale si le
|
||||
// repository est disponible.
|
||||
if (syncedUserRepository != null && !users.isEmpty()) {
|
||||
List<SyncedUserEntity> snapshots = users.stream()
|
||||
.map(user -> {
|
||||
SyncedUserEntity entity = new SyncedUserEntity();
|
||||
entity.setRealmName(realmName);
|
||||
entity.setKeycloakId(user.getId());
|
||||
entity.setUsername(user.getUsername());
|
||||
entity.setEmail(user.getEmail());
|
||||
entity.setEnabled(user.isEnabled());
|
||||
entity.setEmailVerified(user.isEmailVerified());
|
||||
|
||||
if (user.getCreatedTimestamp() != null) {
|
||||
LocalDateTime createdAt = LocalDateTime.ofInstant(
|
||||
Instant.ofEpochMilli(user.getCreatedTimestamp()),
|
||||
ZoneOffset.UTC);
|
||||
entity.setCreatedAt(createdAt);
|
||||
}
|
||||
return entity;
|
||||
})
|
||||
.toList();
|
||||
|
||||
syncedUserRepository.replaceForRealm(realmName, snapshots);
|
||||
log.info("Persisted {} synced user snapshots for realm {}", snapshots.size(), realmName);
|
||||
}
|
||||
|
||||
int count = userReps.size();
|
||||
log.info("✅ {} utilisateurs synchronisés depuis le realm {}", count, realmName);
|
||||
return count;
|
||||
} catch (Exception e) {
|
||||
log.error("❌ Erreur lors de la synchronisation des utilisateurs depuis le realm {}", realmName, e);
|
||||
throw new RuntimeException("Erreur lors de la synchronisation des utilisateurs", e);
|
||||
status = "FAILURE";
|
||||
errorMessage = e.getMessage();
|
||||
throw new RuntimeException("Erreur de synchronisation utilisateurs: " + e.getMessage(), e);
|
||||
} finally {
|
||||
recordSyncHistory(realmName, "USER", status, count, start, errorMessage);
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Logged(action = "SYNC_ROLES", resource = "REALM")
|
||||
public int syncRolesFromRealm(@NotBlank String realmName) {
|
||||
log.info("Synchronisation des rôles depuis le realm: {}", realmName);
|
||||
LocalDateTime start = LocalDateTime.now();
|
||||
int count = 0;
|
||||
String status = "SUCCESS";
|
||||
String errorMessage = null;
|
||||
|
||||
try {
|
||||
List<RoleRepresentation> roleReps = keycloakAdminClient.getInstance()
|
||||
.realm(realmName)
|
||||
.roles()
|
||||
.list();
|
||||
List<RoleRepresentation> roles = keycloak.realm(realmName).roles().list();
|
||||
count = roles.size();
|
||||
|
||||
// Persister un snapshot minimal des rôles dans la base locale si le repository
|
||||
// est disponible.
|
||||
if (syncedRoleRepository != null && !roles.isEmpty()) {
|
||||
List<SyncedRoleEntity> snapshots = roles.stream()
|
||||
.map(role -> {
|
||||
SyncedRoleEntity entity = new SyncedRoleEntity();
|
||||
entity.setRealmName(realmName);
|
||||
entity.setRoleName(role.getName());
|
||||
entity.setDescription(role.getDescription());
|
||||
return entity;
|
||||
})
|
||||
.toList();
|
||||
|
||||
syncedRoleRepository.replaceForRealm(realmName, snapshots);
|
||||
log.info("Persisted {} synced role snapshots for realm {}", snapshots.size(), realmName);
|
||||
}
|
||||
|
||||
int count = roleReps.size();
|
||||
log.info("✅ {} rôles synchronisés depuis le realm {}", count, realmName);
|
||||
return count;
|
||||
} catch (Exception e) {
|
||||
log.error("❌ Erreur lors de la synchronisation des rôles depuis le realm {}", realmName, e);
|
||||
throw new RuntimeException("Erreur lors de la synchronisation des rôles", e);
|
||||
status = "FAILURE";
|
||||
errorMessage = e.getMessage();
|
||||
throw new RuntimeException("Erreur de synchronisation rôles: " + e.getMessage(), e);
|
||||
} finally {
|
||||
recordSyncHistory(realmName, "ROLE", status, count, start, errorMessage);
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@Logged(action = "REALM_SYNC", resource = "SYSTEM")
|
||||
public Map<String, Integer> syncAllRealms() {
|
||||
log.info("Synchronisation de tous les realms");
|
||||
|
||||
Map<String, Integer> results = new java.util.HashMap<>();
|
||||
|
||||
Map<String, Integer> result = new HashMap<>();
|
||||
try {
|
||||
// Lister tous les realms
|
||||
List<org.keycloak.representations.idm.RealmRepresentation> realms =
|
||||
keycloakAdminClient.getInstance().realms().findAll();
|
||||
|
||||
for (org.keycloak.representations.idm.RealmRepresentation realm : realms) {
|
||||
String realmName = realm.getRealm();
|
||||
try {
|
||||
int usersCount = syncUsersFromRealm(realmName);
|
||||
int rolesCount = syncRolesFromRealm(realmName);
|
||||
results.put(realmName, usersCount + rolesCount);
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors de la synchronisation du realm {}", realmName, e);
|
||||
results.put(realmName, 0);
|
||||
// getAllRealms() utilise un HttpClient raw avec ObjectMapper(FAIL_ON_UNKNOWN_PROPERTIES=false)
|
||||
// pour éviter les erreurs de désérialisation de RealmRepresentation avec Keycloak 26+
|
||||
List<String> realmNames = keycloakAdminClient.getAllRealms();
|
||||
|
||||
for (String realmName : realmNames) {
|
||||
if (realmName == null || realmName.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
log.info("Synchronisation complète du realm {}", realmName);
|
||||
int totalForRealm = 0;
|
||||
try {
|
||||
int users = syncUsersFromRealm(realmName);
|
||||
int roles = syncRolesFromRealm(realmName);
|
||||
totalForRealm = users + roles;
|
||||
log.info("✅ Realm {} synchronisé (users={}, roles={})", realmName, users, roles);
|
||||
} catch (Exception e) {
|
||||
log.error("❌ Erreur lors de la synchronisation du realm {}", realmName, e);
|
||||
// On enregistre quand même le realm dans le résultat avec 0 éléments traités
|
||||
totalForRealm = 0;
|
||||
}
|
||||
result.put(realmName, totalForRealm);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors de la synchronisation de tous les realms", e);
|
||||
log.error("❌ Erreur lors de la récupération de la liste des realms pour synchronisation globale", e);
|
||||
// En cas d'erreur globale, on retourne simplement une map vide (aucune
|
||||
// approximation)
|
||||
}
|
||||
|
||||
return results;
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> checkDataConsistency(@NotBlank String realmName) {
|
||||
log.info("Vérification de la cohérence des données pour le realm: {}", realmName);
|
||||
Map<String, Object> report = new HashMap<>();
|
||||
report.put("realmName", realmName);
|
||||
|
||||
Map<String, Object> report = new java.util.HashMap<>();
|
||||
|
||||
try {
|
||||
// Pour l'instant, on retourne juste un rapport basique
|
||||
// En production, on comparerait avec un cache local
|
||||
report.put("realmName", realmName);
|
||||
report.put("status", "ok");
|
||||
report.put("message", "Cohérence vérifiée");
|
||||
// Données actuelles dans Keycloak
|
||||
List<UserRepresentation> kcUsers = keycloak.realm(realmName).users().list();
|
||||
List<RoleRepresentation> kcRoles = keycloak.realm(realmName).roles().list();
|
||||
|
||||
// Snapshots locaux
|
||||
List<SyncedUserEntity> localUsers = syncedUserRepository.list("realmName", realmName);
|
||||
List<SyncedRoleEntity> localRoles = syncedRoleRepository.list("realmName", realmName);
|
||||
|
||||
// Comparaison exacte des identifiants utilisateurs
|
||||
Set<String> kcUserIds = kcUsers.stream()
|
||||
.map(UserRepresentation::getId)
|
||||
.filter(id -> id != null && !id.isBlank())
|
||||
.collect(java.util.stream.Collectors.toSet());
|
||||
|
||||
Set<String> localUserIds = localUsers.stream()
|
||||
.map(SyncedUserEntity::getKeycloakId)
|
||||
.filter(id -> id != null && !id.isBlank())
|
||||
.collect(java.util.stream.Collectors.toSet());
|
||||
|
||||
Set<String> missingUsersInLocal = new HashSet<>(kcUserIds);
|
||||
missingUsersInLocal.removeAll(localUserIds);
|
||||
|
||||
Set<String> missingUsersInKeycloak = new HashSet<>(localUserIds);
|
||||
missingUsersInKeycloak.removeAll(kcUserIds);
|
||||
|
||||
// Comparaison exacte des noms de rôles
|
||||
Set<String> kcRoleNames = kcRoles.stream()
|
||||
.map(RoleRepresentation::getName)
|
||||
.filter(name -> name != null && !name.isBlank())
|
||||
.collect(java.util.stream.Collectors.toSet());
|
||||
|
||||
Set<String> localRoleNames = localRoles.stream()
|
||||
.map(SyncedRoleEntity::getRoleName)
|
||||
.filter(name -> name != null && !name.isBlank())
|
||||
.collect(java.util.stream.Collectors.toSet());
|
||||
|
||||
Set<String> missingRolesInLocal = new HashSet<>(kcRoleNames);
|
||||
missingRolesInLocal.removeAll(localRoleNames);
|
||||
|
||||
Set<String> missingRolesInKeycloak = new HashSet<>(localRoleNames);
|
||||
missingRolesInKeycloak.removeAll(kcRoleNames);
|
||||
|
||||
boolean usersOk = missingUsersInLocal.isEmpty() && missingUsersInKeycloak.isEmpty();
|
||||
boolean rolesOk = missingRolesInLocal.isEmpty() && missingRolesInKeycloak.isEmpty();
|
||||
|
||||
report.put("status", (usersOk && rolesOk) ? "OK" : "MISMATCH");
|
||||
|
||||
report.put("usersKeycloakCount", kcUserIds.size());
|
||||
report.put("usersLocalCount", localUserIds.size());
|
||||
report.put("missingUsersInLocal", missingUsersInLocal);
|
||||
report.put("missingUsersInKeycloak", missingUsersInKeycloak);
|
||||
|
||||
report.put("rolesKeycloakCount", kcRoleNames.size());
|
||||
report.put("rolesLocalCount", localRoleNames.size());
|
||||
report.put("missingRolesInLocal", missingRolesInLocal);
|
||||
report.put("missingRolesInKeycloak", missingRolesInKeycloak);
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors de la vérification de cohérence pour le realm {}", realmName, e);
|
||||
report.put("status", "error");
|
||||
report.put("message", e.getMessage());
|
||||
log.error("❌ Erreur lors du contrôle de cohérence des données pour le realm {}", realmName, e);
|
||||
report.put("status", "ERROR");
|
||||
report.put("error", e.getMessage());
|
||||
}
|
||||
|
||||
return report;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public Map<String, Object> forceSyncRealm(@NotBlank String realmName) {
|
||||
log.info("Synchronisation forcée du realm: {}", realmName);
|
||||
|
||||
Map<String, Object> stats = new java.util.HashMap<>();
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
try {
|
||||
int usersCount = syncUsersFromRealm(realmName);
|
||||
int rolesCount = syncRolesFromRealm(realmName);
|
||||
|
||||
stats.put("realmName", realmName);
|
||||
stats.put("usersCount", usersCount);
|
||||
stats.put("rolesCount", rolesCount);
|
||||
stats.put("success", true);
|
||||
stats.put("durationMs", System.currentTimeMillis() - startTime);
|
||||
int users = syncUsersFromRealm(realmName);
|
||||
int roles = syncRolesFromRealm(realmName);
|
||||
result.put("usersSynced", users);
|
||||
result.put("rolesSynced", roles);
|
||||
result.put("status", "SUCCESS");
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors de la synchronisation forcée du realm {}", realmName, e);
|
||||
stats.put("success", false);
|
||||
stats.put("error", e.getMessage());
|
||||
stats.put("durationMs", System.currentTimeMillis() - startTime);
|
||||
result.put("status", "FAILURE");
|
||||
result.put("error", e.getMessage());
|
||||
}
|
||||
|
||||
return stats;
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getLastSyncStatus(@NotBlank String realmName) {
|
||||
log.debug("Récupération du statut de la dernière synchronisation pour le realm: {}", realmName);
|
||||
List<SyncHistoryEntity> history = syncHistoryRepository.findLatestByRealm(realmName, 1);
|
||||
if (history.isEmpty()) {
|
||||
return Collections.singletonMap("status", "NEVER_SYNCED");
|
||||
}
|
||||
SyncHistoryEntity lastSync = history.get(0);
|
||||
|
||||
Map<String, Object> status = new java.util.HashMap<>();
|
||||
status.put("realmName", realmName);
|
||||
status.put("lastSyncTime", System.currentTimeMillis()); // En production, récupérer depuis un cache
|
||||
status.put("status", "completed");
|
||||
|
||||
return status;
|
||||
Map<String, Object> statusMap = new HashMap<>(); // Utilisation de HashMap pour permettre nulls si besoin
|
||||
statusMap.put("lastSyncDate", lastSync.getSyncDate());
|
||||
statusMap.put("status", lastSync.getStatus());
|
||||
statusMap.put("type", lastSync.getSyncType());
|
||||
statusMap.put("itemsProcessed", lastSync.getItemsProcessed());
|
||||
return statusMap;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isKeycloakAvailable() {
|
||||
log.debug("Vérification de la disponibilité de Keycloak");
|
||||
|
||||
try {
|
||||
// Test de connexion en récupérant les informations du serveur
|
||||
keycloakAdminClient.getInstance().serverInfo().getInfo();
|
||||
log.debug("✅ Keycloak est accessible et fonctionne");
|
||||
// getAllRealms() utilise un HttpClient raw : pas de désérialisation de RealmRepresentation
|
||||
// donc pas d'erreur UnrecognizedPropertyException avec Keycloak 26+
|
||||
keycloakAdminClient.getAllRealms();
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
log.error("❌ Keycloak n'est pas accessible: {}", e.getMessage());
|
||||
log.warn("Keycloak availability check failed: {}", e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getKeycloakHealthInfo() {
|
||||
log.info("Récupération du statut de santé complet de Keycloak");
|
||||
|
||||
Map<String, Object> healthInfo = new java.util.HashMap<>();
|
||||
healthInfo.put("timestamp", System.currentTimeMillis());
|
||||
|
||||
Map<String, Object> health = new HashMap<>();
|
||||
try {
|
||||
// Test connexion principale
|
||||
var serverInfo = keycloakAdminClient.getInstance().serverInfo().getInfo();
|
||||
healthInfo.put("keycloakAccessible", true);
|
||||
healthInfo.put("keycloakVersion", serverInfo.getSystemInfo().getVersion());
|
||||
|
||||
// Test des realms (on essaie juste de lister)
|
||||
try {
|
||||
int realmsCount = keycloakAdminClient.getInstance().realms().findAll().size();
|
||||
healthInfo.put("realmsAccessible", true);
|
||||
healthInfo.put("realmsCount", realmsCount);
|
||||
} catch (Exception e) {
|
||||
healthInfo.put("realmsAccessible", false);
|
||||
log.warn("Impossible d'accéder aux realms: {}", e.getMessage());
|
||||
}
|
||||
|
||||
healthInfo.put("overallHealthy", true);
|
||||
log.info("✅ Keycloak est en bonne santé - Version: {}, Realms: {}",
|
||||
healthInfo.get("keycloakVersion"), healthInfo.get("realmsCount"));
|
||||
|
||||
var info = keycloak.serverInfo().getInfo();
|
||||
health.put("status", "UP");
|
||||
health.put("version", info.getSystemInfo().getVersion());
|
||||
health.put("serverTime", info.getSystemInfo().getServerTime());
|
||||
} catch (Exception e) {
|
||||
healthInfo.put("keycloakAccessible", false);
|
||||
healthInfo.put("overallHealthy", false);
|
||||
healthInfo.put("errorMessage", e.getMessage());
|
||||
log.error("❌ Keycloak n'est pas accessible: {}", e.getMessage());
|
||||
log.debug("serverInfo().getInfo() failed, trying raw HTTP fallback: {}", e.getMessage());
|
||||
fetchVersionViaHttp(health);
|
||||
}
|
||||
return health;
|
||||
}
|
||||
|
||||
return healthInfo;
|
||||
private void fetchVersionViaHttp(Map<String, Object> health) {
|
||||
try {
|
||||
String token = keycloak.tokenManager().getAccessTokenString();
|
||||
var client = java.net.http.HttpClient.newHttpClient();
|
||||
var request = java.net.http.HttpRequest.newBuilder()
|
||||
.uri(java.net.URI.create(keycloakServerUrl + "/admin/serverinfo"))
|
||||
.header("Authorization", "Bearer " + token)
|
||||
.header("Accept", "application/json")
|
||||
.GET().build();
|
||||
var response = client.send(request, java.net.http.HttpResponse.BodyHandlers.ofString());
|
||||
if (response.statusCode() == 200) {
|
||||
String body = response.body();
|
||||
health.put("status", "UP");
|
||||
int sysInfoIdx = body.indexOf("\"systemInfo\"");
|
||||
if (sysInfoIdx >= 0) {
|
||||
extractJsonStringField(body, "version", sysInfoIdx)
|
||||
.ifPresent(v -> health.put("version", v));
|
||||
extractJsonStringField(body, "serverTime", sysInfoIdx)
|
||||
.ifPresent(v -> health.put("serverTime", v));
|
||||
}
|
||||
if (!health.containsKey("version")) {
|
||||
health.put("version", "UP (version non parsée)");
|
||||
}
|
||||
} else {
|
||||
health.put("status", "UP");
|
||||
health.put("version", "UP (serverinfo HTTP " + response.statusCode() + ")");
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
log.error("Fallback HTTP serverinfo also failed: {}", ex.getMessage());
|
||||
health.put("status", "DOWN");
|
||||
health.put("error", ex.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private java.util.Optional<String> extractJsonStringField(String json, String field, int searchFrom) {
|
||||
String pattern = "\"" + field + "\"";
|
||||
int idx = json.indexOf(pattern, searchFrom);
|
||||
if (idx < 0) return java.util.Optional.empty();
|
||||
int colonIdx = json.indexOf(':', idx + pattern.length());
|
||||
if (colonIdx < 0) return java.util.Optional.empty();
|
||||
int startQuote = json.indexOf('"', colonIdx + 1);
|
||||
if (startQuote < 0) return java.util.Optional.empty();
|
||||
int endQuote = json.indexOf('"', startQuote + 1);
|
||||
if (endQuote < 0) return java.util.Optional.empty();
|
||||
return java.util.Optional.of(json.substring(startQuote + 1, endQuote));
|
||||
}
|
||||
|
||||
// Helper method to record history
|
||||
private void recordSyncHistory(String realmName, String type, String status, int count, LocalDateTime start,
|
||||
String errorMessage) {
|
||||
try {
|
||||
SyncHistoryEntity history = new SyncHistoryEntity();
|
||||
history.setRealmName(realmName);
|
||||
history.setSyncType(type);
|
||||
history.setStatus(status);
|
||||
history.setItemsProcessed(count);
|
||||
history.setSyncDate(LocalDateTime.now());
|
||||
history.setDurationMs(ChronoUnit.MILLIS.between(start, LocalDateTime.now()));
|
||||
history.setErrorMessage(errorMessage);
|
||||
|
||||
// Persist the history entity
|
||||
syncHistoryRepository.persist(history);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to record sync history", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
package dev.lions.user.manager.service.impl;
|
||||
|
||||
import dev.lions.user.manager.server.impl.interceptor.Logged;
|
||||
|
||||
import dev.lions.user.manager.client.KeycloakAdminClient;
|
||||
import dev.lions.user.manager.dto.importexport.ImportResultDTO;
|
||||
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.enums.user.StatutUser;
|
||||
import dev.lions.user.manager.mapper.UserMapper;
|
||||
import dev.lions.user.manager.service.UserService;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
@@ -19,9 +23,9 @@ import org.keycloak.representations.idm.CredentialRepresentation;
|
||||
import org.keycloak.representations.idm.RoleRepresentation;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
@@ -49,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
|
||||
@@ -81,6 +83,23 @@ public class UserServiceImpl implements UserService {
|
||||
// Convertir en DTOs
|
||||
List<UserDTO> userDTOs = UserMapper.toDTOList(users, realmName);
|
||||
|
||||
// Enrichir avec les rôles si demandé
|
||||
if (Boolean.TRUE.equals(criteria.getIncludeRoles())) {
|
||||
for (UserDTO dto : userDTOs) {
|
||||
try {
|
||||
List<RoleRepresentation> realmRoles = usersResource.get(dto.getId())
|
||||
.roles().realmLevel().listAll();
|
||||
if (realmRoles != null) {
|
||||
dto.setRealmRoles(realmRoles.stream()
|
||||
.map(RoleRepresentation::getName)
|
||||
.collect(Collectors.toList()));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("Impossible de charger les rôles pour l'utilisateur {}: {}", dto.getId(), e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Compter le total
|
||||
long totalCount = usersResource.count();
|
||||
|
||||
@@ -101,21 +120,22 @@ public class UserServiceImpl implements UserService {
|
||||
UserResource userResource = keycloakAdminClient.getUsers(realmName).get(userId);
|
||||
UserRepresentation userRep = userResource.toRepresentation();
|
||||
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());
|
||||
.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());
|
||||
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);
|
||||
@@ -123,10 +143,11 @@ public class UserServiceImpl implements UserService {
|
||||
} 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);
|
||||
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);
|
||||
@@ -140,7 +161,7 @@ public class UserServiceImpl implements UserService {
|
||||
|
||||
try {
|
||||
List<UserRepresentation> users = keycloakAdminClient.getUsers(realmName)
|
||||
.search(username, 0, 1, true);
|
||||
.search(username, 0, 1, true);
|
||||
|
||||
if (users.isEmpty()) {
|
||||
return Optional.empty();
|
||||
@@ -159,7 +180,7 @@ public class UserServiceImpl implements UserService {
|
||||
|
||||
try {
|
||||
List<UserRepresentation> users = keycloakAdminClient.getUsers(realmName)
|
||||
.searchByEmail(email, true);
|
||||
.searchByEmail(email, true);
|
||||
|
||||
if (users.isEmpty()) {
|
||||
return Optional.empty();
|
||||
@@ -173,6 +194,7 @@ public class UserServiceImpl implements UserService {
|
||||
}
|
||||
|
||||
@Override
|
||||
@Logged(action = "USER_CREATE", resource = "USER")
|
||||
public UserDTO createUser(@Valid @NotNull UserDTO user, @NotBlank String realmName) {
|
||||
log.info("Création de l'utilisateur {} dans le realm {}", user.getUsername(), realmName);
|
||||
|
||||
@@ -203,7 +225,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éé
|
||||
@@ -220,6 +242,7 @@ public class UserServiceImpl implements UserService {
|
||||
}
|
||||
|
||||
@Override
|
||||
@Logged(action = "USER_UPDATE", resource = "USER")
|
||||
public UserDTO updateUser(@NotBlank String userId, @Valid @NotNull UserDTO user, @NotBlank String realmName) {
|
||||
log.info("Mise à jour de l'utilisateur {} dans le realm {}", userId, realmName);
|
||||
|
||||
@@ -268,6 +291,7 @@ public class UserServiceImpl implements UserService {
|
||||
}
|
||||
|
||||
@Override
|
||||
@Logged(action = "USER_DELETE", resource = "USER")
|
||||
public void deleteUser(@NotBlank String userId, @NotBlank String realmName, boolean hardDelete) {
|
||||
log.info("Suppression de l'utilisateur {} dans le realm {} (hard: {})", userId, realmName, hardDelete);
|
||||
|
||||
@@ -296,6 +320,7 @@ public class UserServiceImpl implements UserService {
|
||||
}
|
||||
|
||||
@Override
|
||||
@Logged(action = "USER_ACTIVATE", resource = "USER")
|
||||
public void activateUser(@NotBlank String userId, @NotBlank String realmName) {
|
||||
log.info("Activation de l'utilisateur {} dans le realm {}", userId, realmName);
|
||||
|
||||
@@ -313,6 +338,7 @@ public class UserServiceImpl implements UserService {
|
||||
}
|
||||
|
||||
@Override
|
||||
@Logged(action = "USER_DEACTIVATE", resource = "USER")
|
||||
public void deactivateUser(@NotBlank String userId, @NotBlank String realmName, String raison) {
|
||||
log.info("Désactivation de l'utilisateur {} dans le realm {} (raison: {})", userId, realmName, raison);
|
||||
|
||||
@@ -332,20 +358,22 @@ public class UserServiceImpl implements UserService {
|
||||
@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);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Logged(action = "USER_UNLOCK", resource = "USER")
|
||||
public void unlockUser(@NotBlank String userId, @NotBlank String realmName) {
|
||||
log.info("Déverrouillage de l'utilisateur {} dans le realm {}", userId, realmName);
|
||||
activateUser(userId, realmName);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Logged(action = "USER_PASSWORD_RESET", resource = "USER")
|
||||
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);
|
||||
@@ -367,6 +395,7 @@ public class UserServiceImpl implements UserService {
|
||||
}
|
||||
|
||||
@Override
|
||||
@Logged(action = "USER_FORCE_LOGOUT", resource = "USER")
|
||||
public int logoutAllSessions(@NotBlank String userId, @NotBlank String realmName) {
|
||||
log.info("Déconnexion de toutes les sessions pour l'utilisateur {}", userId);
|
||||
|
||||
@@ -390,8 +419,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();
|
||||
@@ -411,10 +440,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);
|
||||
}
|
||||
@@ -423,7 +452,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);
|
||||
@@ -435,7 +464,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);
|
||||
@@ -445,14 +474,223 @@ 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 CSV des utilisateurs avec critères: {}", criteria);
|
||||
|
||||
try {
|
||||
// Récupérer tous les utilisateurs correspondant aux critères
|
||||
UserSearchResultDTO searchResult = searchUsers(criteria);
|
||||
List<UserDTO> users = searchResult.getUsers();
|
||||
|
||||
if (users == null || users.isEmpty()) {
|
||||
log.warn("Aucun utilisateur trouvé pour l'export CSV");
|
||||
return generateCSVHeader();
|
||||
}
|
||||
|
||||
// Générer le CSV
|
||||
StringBuilder csv = new StringBuilder();
|
||||
csv.append(generateCSVHeader());
|
||||
csv.append("\n");
|
||||
|
||||
for (UserDTO user : users) {
|
||||
csv.append(escapeCSVField(user.getUsername() != null ? user.getUsername() : ""));
|
||||
csv.append(",");
|
||||
csv.append(escapeCSVField(user.getEmail() != null ? user.getEmail() : ""));
|
||||
csv.append(",");
|
||||
csv.append(escapeCSVField(user.getPrenom() != null ? user.getPrenom() : ""));
|
||||
csv.append(",");
|
||||
csv.append(escapeCSVField(user.getNom() != null ? user.getNom() : ""));
|
||||
csv.append(",");
|
||||
csv.append(escapeCSVField(user.getTelephone() != null ? user.getTelephone() : ""));
|
||||
csv.append(",");
|
||||
csv.append(user.getEnabled() != null && user.getEnabled() ? "true" : "false");
|
||||
csv.append(",");
|
||||
csv.append(user.getEmailVerified() != null && user.getEmailVerified() ? "true" : "false");
|
||||
csv.append(",");
|
||||
csv.append(escapeCSVField(user.getStatut() != null ? user.getStatut().name() : ""));
|
||||
csv.append(",");
|
||||
csv.append(escapeCSVField(user.getOrganisation() != null ? user.getOrganisation() : ""));
|
||||
csv.append(",");
|
||||
csv.append(escapeCSVField(user.getDepartement() != null ? user.getDepartement() : ""));
|
||||
csv.append(",");
|
||||
csv.append(escapeCSVField(user.getFonction() != null ? user.getFonction() : ""));
|
||||
csv.append(",");
|
||||
csv.append(escapeCSVField(user.getPays() != null ? user.getPays() : ""));
|
||||
csv.append(",");
|
||||
csv.append(escapeCSVField(user.getVille() != null ? user.getVille() : ""));
|
||||
csv.append(",");
|
||||
csv.append(escapeCSVField(user.getDateCreation() != null
|
||||
? user.getDateCreation().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)
|
||||
: ""));
|
||||
csv.append("\n");
|
||||
}
|
||||
|
||||
log.info("✅ Export CSV réussi: {} utilisateur(s) exporté(s)", users.size());
|
||||
return csv.toString();
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("❌ Erreur lors de l'export CSV", e);
|
||||
throw new RuntimeException("Impossible d'exporter les utilisateurs en CSV", e);
|
||||
}
|
||||
}
|
||||
|
||||
private String generateCSVHeader() {
|
||||
return "username,email,prenom,nom,telephone,enabled,emailVerified,statut,organisation,departement,fonction,pays,ville,dateCreation";
|
||||
}
|
||||
|
||||
private String escapeCSVField(String field) {
|
||||
if (field == null) {
|
||||
return "";
|
||||
}
|
||||
// Si le champ contient une virgule, des guillemets ou un saut de ligne,
|
||||
// l'entourer de guillemets
|
||||
if (field.contains(",") || field.contains("\"") || field.contains("\n")) {
|
||||
// Échapper les guillemets en les doublant
|
||||
return "\"" + field.replace("\"", "\"\"") + "\"";
|
||||
}
|
||||
return field;
|
||||
}
|
||||
|
||||
@Override
|
||||
public dev.lions.user.manager.dto.importexport.ImportResultDTO importUsersFromCSV(@NotBlank String csvContent, @NotBlank String realmName) {
|
||||
// TODO: Implémenter l'import CSV
|
||||
throw new UnsupportedOperationException("Import CSV non implémenté");
|
||||
@Logged(action = "USER_IMPORT", resource = "USER")
|
||||
public ImportResultDTO importUsersFromCSV(@NotBlank String csvContent, @NotBlank String realmName) {
|
||||
log.info("Import CSV de {} lignes pour le realm {}", csvContent.split("\n").length, realmName);
|
||||
|
||||
ImportResultDTO.ImportResultDTOBuilder resultBuilder = ImportResultDTO.builder();
|
||||
List<ImportResultDTO.ImportErrorDTO> errors = new ArrayList<>();
|
||||
int successCount = 0;
|
||||
int lineNumber = 0;
|
||||
|
||||
String[] lines = csvContent.split("\n");
|
||||
resultBuilder.totalLines(lines.length);
|
||||
|
||||
// Ignorer la première ligne si c'est l'en-tête
|
||||
int startLine = 0;
|
||||
if (lines.length > 0 && lines[0].toLowerCase().contains("username")) {
|
||||
startLine = 1;
|
||||
}
|
||||
|
||||
for (int i = startLine; i < lines.length; i++) {
|
||||
lineNumber = i + 1;
|
||||
String line = lines[i].trim();
|
||||
|
||||
if (line.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
// Parser la ligne CSV
|
||||
String[] fields = parseCSVLine(line);
|
||||
|
||||
if (fields.length < 4) {
|
||||
errors.add(ImportResultDTO.ImportErrorDTO.builder()
|
||||
.lineNumber(lineNumber)
|
||||
.lineContent(line)
|
||||
.errorType(ImportResultDTO.ErrorType.INVALID_FORMAT)
|
||||
.message("Nombre de colonnes insuffisant (minimum 4: username, email, prenom, nom)")
|
||||
.build());
|
||||
continue;
|
||||
}
|
||||
|
||||
// Créer l'utilisateur
|
||||
String username = fields[0].trim();
|
||||
String email = fields[1].trim();
|
||||
String prenom = fields.length > 2 ? fields[2].trim() : "";
|
||||
String nom = fields.length > 3 ? fields[3].trim() : "";
|
||||
String telephone = fields.length > 4 ? fields[4].trim() : null;
|
||||
Boolean enabled = fields.length > 5 ? Boolean.parseBoolean(fields[5].trim()) : true;
|
||||
Boolean emailVerified = fields.length > 6 ? Boolean.parseBoolean(fields[6].trim()) : false;
|
||||
|
||||
// Valider les champs obligatoires
|
||||
if (username.isBlank() || email.isBlank()) {
|
||||
errors.add(ImportResultDTO.ImportErrorDTO.builder()
|
||||
.lineNumber(lineNumber)
|
||||
.lineContent(line)
|
||||
.errorType(ImportResultDTO.ErrorType.VALIDATION_ERROR)
|
||||
.field(username.isBlank() ? "username" : "email")
|
||||
.message("Le username et l'email sont obligatoires")
|
||||
.build());
|
||||
continue;
|
||||
}
|
||||
|
||||
// Vérifier si l'utilisateur existe déjà
|
||||
if (emailExists(email, realmName)) {
|
||||
errors.add(ImportResultDTO.ImportErrorDTO.builder()
|
||||
.lineNumber(lineNumber)
|
||||
.lineContent(line)
|
||||
.errorType(ImportResultDTO.ErrorType.DUPLICATE_USER)
|
||||
.field("email")
|
||||
.message("Un utilisateur avec cet email existe déjà")
|
||||
.build());
|
||||
continue;
|
||||
}
|
||||
|
||||
// Créer l'utilisateur
|
||||
UserDTO newUser = UserDTO.builder()
|
||||
.username(username)
|
||||
.email(email)
|
||||
.prenom(prenom)
|
||||
.nom(nom)
|
||||
.telephone(telephone)
|
||||
.enabled(enabled)
|
||||
.emailVerified(emailVerified)
|
||||
.build();
|
||||
|
||||
createUser(newUser, realmName);
|
||||
successCount++;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Erreur lors de l'import de la ligne {}: {}", lineNumber, e.getMessage());
|
||||
errors.add(ImportResultDTO.ImportErrorDTO.builder()
|
||||
.lineNumber(lineNumber)
|
||||
.lineContent(line)
|
||||
.errorType(ImportResultDTO.ErrorType.CREATION_ERROR)
|
||||
.message("Erreur lors de la création: " + e.getMessage())
|
||||
.details(e.getClass().getSimpleName())
|
||||
.build());
|
||||
}
|
||||
}
|
||||
|
||||
resultBuilder.successCount(successCount);
|
||||
resultBuilder.errors(errors);
|
||||
resultBuilder.errorCount(errors.size());
|
||||
|
||||
ImportResultDTO result = resultBuilder.build();
|
||||
result.generateMessage();
|
||||
|
||||
log.info("✅ Import CSV terminé: {} succès, {} erreurs", successCount, errors.size());
|
||||
return result;
|
||||
}
|
||||
|
||||
private String[] parseCSVLine(String line) {
|
||||
List<String> fields = new ArrayList<>();
|
||||
boolean inQuotes = false;
|
||||
StringBuilder currentField = new StringBuilder();
|
||||
|
||||
for (int i = 0; i < line.length(); i++) {
|
||||
char c = line.charAt(i);
|
||||
|
||||
if (c == '"') {
|
||||
if (inQuotes && i + 1 < line.length() && line.charAt(i + 1) == '"') {
|
||||
// Guillemet échappé
|
||||
currentField.append('"');
|
||||
i++; // Passer le guillemet suivant
|
||||
} else {
|
||||
// Toggle inQuotes
|
||||
inQuotes = !inQuotes;
|
||||
}
|
||||
} else if (c == ',' && !inQuotes) {
|
||||
// Fin du champ
|
||||
fields.add(currentField.toString());
|
||||
currentField = new StringBuilder();
|
||||
} else {
|
||||
currentField.append(c);
|
||||
}
|
||||
}
|
||||
|
||||
// Ajouter le dernier champ
|
||||
fields.add(currentField.toString());
|
||||
|
||||
return fields.toArray(new String[0]);
|
||||
}
|
||||
|
||||
// ==================== Méthodes privées ====================
|
||||
@@ -477,21 +715,156 @@ public class UserServiceImpl implements UserService {
|
||||
|
||||
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
|
||||
// Filtrer par username
|
||||
if (criteria.getUsername() != null
|
||||
&& !criteria.getUsername().equalsIgnoreCase(user.getUsername())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
// Filtrer par email
|
||||
if (criteria.getEmail() != null && !criteria.getEmail().equalsIgnoreCase(user.getEmail())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Filtrer par prénom
|
||||
if (criteria.getPrenom() != null) {
|
||||
String prenom = user.getFirstName();
|
||||
if (prenom == null || !prenom.toLowerCase().contains(criteria.getPrenom().toLowerCase())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Filtrer par nom
|
||||
if (criteria.getNom() != null) {
|
||||
String nom = user.getLastName();
|
||||
if (nom == null || !nom.toLowerCase().contains(criteria.getNom().toLowerCase())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Filtrer par téléphone
|
||||
if (criteria.getTelephone() != null) {
|
||||
String phone = user.getAttributes() != null
|
||||
? user.getAttributes().getOrDefault("phone_number", Collections.emptyList()).stream()
|
||||
.findFirst().orElse(null)
|
||||
: null;
|
||||
if (phone == null || !phone.contains(criteria.getTelephone())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Filtrer par statut (basé sur enabled et emailVerified)
|
||||
if (criteria.getStatut() != null) {
|
||||
StatutUser userStatut = determineUserStatut(user);
|
||||
if (!criteria.getStatut().equals(userStatut)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Filtrer par organisation
|
||||
if (criteria.getOrganisation() != null) {
|
||||
String org = user.getAttributes() != null
|
||||
? user.getAttributes().getOrDefault("organization", Collections.emptyList()).stream()
|
||||
.findFirst().orElse(null)
|
||||
: null;
|
||||
if (org == null || !org.toLowerCase().contains(criteria.getOrganisation().toLowerCase())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Filtrer par département
|
||||
if (criteria.getDepartement() != null) {
|
||||
String dept = user.getAttributes() != null
|
||||
? user.getAttributes().getOrDefault("department", Collections.emptyList()).stream()
|
||||
.findFirst().orElse(null)
|
||||
: null;
|
||||
if (dept == null || !dept.toLowerCase().contains(criteria.getDepartement().toLowerCase())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Filtrer par fonction
|
||||
if (criteria.getFonction() != null) {
|
||||
String fonction = user.getAttributes() != null
|
||||
? user.getAttributes().getOrDefault("job_title", Collections.emptyList()).stream()
|
||||
.findFirst().orElse(null)
|
||||
: null;
|
||||
if (fonction == null
|
||||
|| !fonction.toLowerCase().contains(criteria.getFonction().toLowerCase())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Filtrer par pays
|
||||
if (criteria.getPays() != null) {
|
||||
String pays = user.getAttributes() != null
|
||||
? user.getAttributes().getOrDefault("country", Collections.emptyList()).stream()
|
||||
.findFirst().orElse(null)
|
||||
: null;
|
||||
if (pays == null || !pays.toLowerCase().contains(criteria.getPays().toLowerCase())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Filtrer par ville
|
||||
if (criteria.getVille() != null) {
|
||||
String ville = user.getAttributes() != null
|
||||
? user.getAttributes().getOrDefault("city", Collections.emptyList()).stream()
|
||||
.findFirst().orElse(null)
|
||||
: null;
|
||||
if (ville == null || !ville.toLowerCase().contains(criteria.getVille().toLowerCase())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Filtrer par date de création
|
||||
if (criteria.getDateCreationMin() != null || criteria.getDateCreationMax() != null) {
|
||||
Long createdTimestamp = user.getCreatedTimestamp();
|
||||
if (createdTimestamp != null) {
|
||||
LocalDateTime createdDate = LocalDateTime.ofEpochSecond(
|
||||
createdTimestamp / 1000, 0,
|
||||
java.time.ZoneOffset.UTC);
|
||||
|
||||
if (criteria.getDateCreationMin() != null &&
|
||||
createdDate.isBefore(criteria.getDateCreationMin())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (criteria.getDateCreationMax() != null &&
|
||||
createdDate.isAfter(criteria.getDateCreationMax())) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// Si pas de date de création et qu'un filtre min est défini, exclure
|
||||
if (criteria.getDateCreationMin() != null) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private StatutUser determineUserStatut(UserRepresentation user) {
|
||||
if (!user.isEnabled()) {
|
||||
return StatutUser.INACTIF;
|
||||
}
|
||||
// Si enabled mais email non vérifié, on considère toujours comme ACTIF
|
||||
// car l'email non vérifié n'empêche pas l'activation du compte
|
||||
return StatutUser.ACTIF;
|
||||
}
|
||||
}
|
||||
|
||||
6
src/main/resources/META-INF/beans.xml
Normal file
6
src/main/resources/META-INF/beans.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<beans xmlns="https://jakarta.ee/xml/ns/jakartajsf"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartajsf https://jakarta.ee/xml/ns/jakartajsf/beans_3_0.xsd"
|
||||
bean-discovery-mode="annotated">
|
||||
</beans>
|
||||
33
src/main/resources/META-INF/reflection-config.json
Normal file
33
src/main/resources/META-INF/reflection-config.json
Normal file
@@ -0,0 +1,33 @@
|
||||
[
|
||||
{
|
||||
"name": "dev.lions.user.manager.dto.realm.RealmAssignmentDTO",
|
||||
"allDeclaredFields": true,
|
||||
"allDeclaredMethods": true,
|
||||
"allDeclaredConstructors": true
|
||||
},
|
||||
{
|
||||
"name": "dev.lions.user.manager.dto.role.RoleDTO",
|
||||
"allDeclaredFields": true,
|
||||
"allDeclaredMethods": true,
|
||||
"allDeclaredConstructors": true
|
||||
},
|
||||
{
|
||||
"name": "dev.lions.user.manager.dto.role.RoleDTO$RoleCompositeDTO",
|
||||
"allDeclaredFields": true,
|
||||
"allDeclaredMethods": true,
|
||||
"allDeclaredConstructors": true
|
||||
},
|
||||
{
|
||||
"name": "dev.lions.user.manager.dto.user.UserDTO",
|
||||
"allDeclaredFields": true,
|
||||
"allDeclaredMethods": true,
|
||||
"allDeclaredConstructors": true
|
||||
},
|
||||
{
|
||||
"name": "dev.lions.user.manager.dto.user.UserSearchResultDTO",
|
||||
"allDeclaredFields": true,
|
||||
"allDeclaredMethods": true,
|
||||
"allDeclaredConstructors": true
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,128 +1,101 @@
|
||||
# ============================================================================
|
||||
# Lions User Manager - Server Implementation Configuration - DEV
|
||||
# ============================================================================
|
||||
# Ce fichier contient UNIQUEMENT les propriétés spécifiques au DÉVELOPPEMENT
|
||||
# Il surcharge 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=*
|
||||
quarkus.http.cors.origins=http://localhost:3000,http://localhost:8080,http://localhost:8081,http://localhost:8082
|
||||
|
||||
# Keycloak OIDC Configuration (DEV)
|
||||
# Le backend vérifie les tokens JWT envoyés par le client
|
||||
# IMPORTANT: Pour un service, Quarkus valide les tokens JWT sans avoir besoin d'un client-id/secret
|
||||
# Le backend accepte les tokens émis pour n'importe quel client du realm
|
||||
# ============================================
|
||||
# OIDC Configuration DEV
|
||||
# ============================================
|
||||
quarkus.oidc.enabled=true
|
||||
quarkus.oidc.auth-server-url=http://localhost:8180/realms/lions-user-manager
|
||||
quarkus.oidc.application-type=service
|
||||
quarkus.oidc.tls.verification=none
|
||||
quarkus.oidc.token.issuer=http://localhost:8180/realms/lions-user-manager
|
||||
quarkus.oidc.discovery-enabled=true
|
||||
# Accepter les tokens avec audience "account" (audience par défaut de Keycloak)
|
||||
# Cela permet d'accepter les tokens émis pour le frontend sans configuration Keycloak supplémentaire
|
||||
quarkus.oidc.token.audience=account
|
||||
# Vérifier le token (obligatoire pour un service)
|
||||
quarkus.oidc.verify-access-token=true
|
||||
quarkus.oidc.tls.verification=none
|
||||
|
||||
# Keycloak Admin Client Configuration (DEV)
|
||||
# ============================================
|
||||
# Keycloak Admin Client Configuration DEV
|
||||
# ============================================
|
||||
lions.keycloak.server-url=http://localhost:8180
|
||||
lions.keycloak.admin-realm=master
|
||||
lions.keycloak.admin-client-id=admin-cli
|
||||
lions.keycloak.admin-username=admin
|
||||
lions.keycloak.admin-password=admin
|
||||
lions.keycloak.connection-pool-size=5
|
||||
lions.keycloak.timeout-seconds=30
|
||||
|
||||
# Realms autorisés (DEV)
|
||||
lions.keycloak.authorized-realms=lions-user-manager,master,btpxpress,test-realm
|
||||
|
||||
# Circuit Breaker Configuration (DEV - plus permissif)
|
||||
quarkus.smallrye-fault-tolerance.enabled=true
|
||||
# Quarkus-managed Keycloak Admin Client DEV
|
||||
quarkus.keycloak.admin-client.server-url=${lions.keycloak.server-url}
|
||||
quarkus.keycloak.admin-client.username=${lions.keycloak.admin-username}
|
||||
quarkus.keycloak.admin-client.password=${lions.keycloak.admin-password}
|
||||
|
||||
# Retry Configuration (DEV)
|
||||
lions.keycloak.retry.max-attempts=3
|
||||
lions.keycloak.retry.delay-seconds=1
|
||||
|
||||
# Audit Configuration (DEV)
|
||||
lions.audit.enabled=true
|
||||
# ============================================
|
||||
# Audit Configuration DEV
|
||||
# ============================================
|
||||
lions.audit.log-to-database=false
|
||||
lions.audit.log-to-file=true
|
||||
lions.audit.retention-days=30
|
||||
|
||||
# Database Configuration (DEV - optionnel)
|
||||
# Décommenter pour utiliser une DB locale
|
||||
#quarkus.datasource.db-kind=postgresql
|
||||
#quarkus.datasource.username=postgres
|
||||
#quarkus.datasource.password=postgres
|
||||
#quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:5432/lions_audit_dev
|
||||
#quarkus.hibernate-orm.database.generation=update
|
||||
#quarkus.flyway.migrate-at-start=false
|
||||
# ============================================
|
||||
# Database Configuration DEV
|
||||
# ============================================
|
||||
quarkus.datasource.health.enabled=false
|
||||
quarkus.datasource.username=${DB_USERNAME:skyfile}
|
||||
quarkus.datasource.password=${DB_PASSWORD:skyfile}
|
||||
quarkus.datasource.jdbc.url=jdbc:postgresql://${DB_HOST:localhost}:${DB_PORT:5432}/${DB_NAME:lions_user_manager_dev}
|
||||
|
||||
# Logging Configuration (DEV)
|
||||
# ============================================
|
||||
# Hibernate ORM Configuration DEV
|
||||
# ============================================
|
||||
quarkus.hibernate-orm.database.generation=update
|
||||
quarkus.hibernate-orm.log.sql=true
|
||||
|
||||
# ============================================
|
||||
# Flyway Configuration DEV
|
||||
# ============================================
|
||||
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."dev.lions.user.manager.security".level=DEBUG
|
||||
quarkus.log.category."org.keycloak".level=INFO
|
||||
quarkus.log.category."io.quarkus".level=INFO
|
||||
# Logging OIDC pour debug
|
||||
quarkus.log.category."io.quarkus.oidc".level=DEBUG
|
||||
quarkus.log.category."io.quarkus.oidc".level=INFO
|
||||
quarkus.log.category."io.quarkus.oidc.runtime".level=DEBUG
|
||||
quarkus.log.category."io.quarkus.security".level=DEBUG
|
||||
quarkus.log.category."io.quarkus.security.runtime".level=DEBUG
|
||||
|
||||
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)
|
||||
# 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é)
|
||||
# ============================================
|
||||
# OpenAPI/Swagger Configuration DEV
|
||||
# ============================================
|
||||
quarkus.swagger-ui.always-include=true
|
||||
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)
|
||||
# ============================================
|
||||
# Dev Services DEV
|
||||
# ============================================
|
||||
quarkus.devservices.enabled=false
|
||||
|
||||
# Security Configuration (DEV)
|
||||
# La sécurité est activée - les rôles sont vérifiés via OIDC/Keycloak
|
||||
# Note: KeycloakTestUserConfig configure automatiquement l'utilisateur de test au démarrage
|
||||
quarkus.security.auth.enabled=true
|
||||
quarkus.security.jaxrs.deny-unannotated-endpoints=false
|
||||
quarkus.security.auth.proactive=false
|
||||
|
||||
# Configuration OIDC - Extraction des rôles
|
||||
# Le backend extrait les rôles depuis realm_access/roles (standard Keycloak)
|
||||
# Le scope "roles" de Keycloak crée automatiquement realm_access.roles
|
||||
# Syntaxe Quarkus: utiliser un slash pour les chemins imbriqués
|
||||
quarkus.oidc.roles.role-claim-path=realm_access/roles
|
||||
|
||||
# Définir explicitement le profil pour que DevSecurityContextProducer le détecte
|
||||
quarkus.profile=dev
|
||||
|
||||
# Logging pour debug du filtre de sécurité
|
||||
quarkus.log.category."dev.lions.user.manager.security".level=DEBUG
|
||||
|
||||
# Logging OIDC et Security pour 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.category."io.quarkus.security.runtime".level=DEBUG
|
||||
|
||||
# Hot Reload
|
||||
# ============================================
|
||||
# Hot Reload DEV
|
||||
# ============================================
|
||||
quarkus.live-reload.instrumentation=true
|
||||
|
||||
# Désactiver le continuous testing qui bloque le démarrage
|
||||
quarkus.test.continuous-testing=disabled
|
||||
|
||||
# Indexer les dépendances Keycloak pour éviter les warnings
|
||||
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
|
||||
|
||||
@@ -1,69 +1,39 @@
|
||||
# ============================================================================
|
||||
# Lions User Manager Server - Configuration Production
|
||||
# Lions User Manager - Server Implementation Configuration - PROD
|
||||
# ============================================================================
|
||||
# Ce fichier contient TOUTES les propriétés spécifiques à la production
|
||||
# Il surcharge et complète application.properties
|
||||
# Ce fichier contient UNIQUEMENT les propriétés spécifiques à la PRODUCTION
|
||||
# Il surcharge application.properties
|
||||
# ============================================================================
|
||||
|
||||
# ============================================
|
||||
# HTTP Configuration PROD
|
||||
# ============================================
|
||||
quarkus.http.port=8080
|
||||
|
||||
# CORS restrictif en production (via variable d'environnement) - autoriser le frontend users.lions.dev
|
||||
quarkus.http.cors.origins=${CORS_ORIGINS:https://users.lions.dev,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 désactivé en prod Kubernetes (utilise stdout pour logs centralisés)
|
||||
# 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
|
||||
# OIDC Configuration PROD
|
||||
# ============================================
|
||||
quarkus.oidc.enabled=true
|
||||
quarkus.oidc.auth-server-url=${KEYCLOAK_AUTH_SERVER_URL:https://security.lions.dev/realms/lions-user-manager}
|
||||
quarkus.oidc.client-id=${KEYCLOAK_CLIENT_ID:lions-user-manager-server}
|
||||
# Client bearer-only - pas de secret nécessaire
|
||||
# quarkus.oidc.credentials.secret=${KEYCLOAK_CLIENT_SECRET}
|
||||
quarkus.oidc.token.issuer=${KEYCLOAK_AUTH_SERVER_URL:https://security.lions.dev/realms/lions-user-manager}
|
||||
|
||||
# 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 # Propriété non reconnue
|
||||
|
||||
# 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:admin}
|
||||
lions.keycloak.admin-password=${KEYCLOAK_ADMIN_PASSWORD:KeycloakAdmin2025!}
|
||||
|
||||
# Pool de connexions augmenté en production
|
||||
lions.keycloak.admin-username=${KEYCLOAK_ADMIN_USERNAME}
|
||||
lions.keycloak.admin-password=${KEYCLOAK_ADMIN_PASSWORD}
|
||||
lions.keycloak.connection-pool-size=20
|
||||
lions.keycloak.timeout-seconds=60
|
||||
|
||||
# Realms autorisés en production (via variable d'environnement)
|
||||
lions.keycloak.authorized-realms=${KEYCLOAK_AUTHORIZED_REALMS:lions-user-manager,btpxpress,master,unionflow}
|
||||
|
||||
# Quarkus-managed Keycloak Admin Client PROD
|
||||
quarkus.keycloak.admin-client.server-url=${lions.keycloak.server-url}
|
||||
quarkus.keycloak.admin-client.username=${lions.keycloak.admin-username}
|
||||
quarkus.keycloak.admin-client.password=${lions.keycloak.admin-password}
|
||||
|
||||
# ============================================
|
||||
# Retry Configuration PROD
|
||||
# ============================================
|
||||
@@ -73,32 +43,51 @@ lions.keycloak.retry.delay-seconds=3
|
||||
# ============================================
|
||||
# Audit Configuration PROD
|
||||
# ============================================
|
||||
lions.audit.retention-days=365
|
||||
lions.audit.log-to-database=true
|
||||
lions.audit.log-to-file=false
|
||||
lions.audit.retention-days=365
|
||||
|
||||
# ============================================
|
||||
# Database Configuration PROD - Désactivé complètement
|
||||
# Database Configuration PROD
|
||||
# ============================================
|
||||
quarkus.datasource.devservices.enabled=false
|
||||
quarkus.datasource.health.enabled=false
|
||||
quarkus.datasource.health.enabled=true
|
||||
quarkus.datasource.username=${DB_USERNAME}
|
||||
quarkus.datasource.password=${DB_PASSWORD}
|
||||
quarkus.datasource.jdbc.url=jdbc:postgresql://${DB_HOST}:${DB_PORT:5432}/${DB_NAME:lions_user_manager}
|
||||
|
||||
# ============================================
|
||||
# Hibernate ORM Configuration PROD
|
||||
# ============================================
|
||||
quarkus.hibernate-orm.database.generation=none
|
||||
quarkus.hibernate-orm.log.sql=false
|
||||
|
||||
# ============================================
|
||||
# Flyway Configuration PROD
|
||||
# ============================================
|
||||
quarkus.flyway.migrate-at-start=true
|
||||
|
||||
# ============================================
|
||||
# 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=INFO
|
||||
|
||||
quarkus.log.console.enable=true
|
||||
quarkus.log.console.format=%d{yyyy-MM-dd HH:mm:ss,SSS} %-5p [%c{3.}] (%t) %s%e%n
|
||||
|
||||
# File Logging désactivé en PROD (logs centralisés via Kubernetes)
|
||||
quarkus.log.file.enable=false
|
||||
|
||||
# ============================================
|
||||
# OpenAPI/Swagger Configuration PROD
|
||||
# ============================================
|
||||
# Swagger désactivé en production par défaut (build-time property)
|
||||
# quarkus.swagger-ui.always-include=false
|
||||
quarkus.swagger-ui.always-include=false
|
||||
quarkus.swagger-ui.enable=false
|
||||
|
||||
# ============================================
|
||||
# Security Configuration PROD (strict)
|
||||
# ============================================
|
||||
# Ces propriétés sont build-time, configurées dans application.properties
|
||||
# quarkus.security.auth.enabled=true
|
||||
# quarkus.security.jaxrs.deny-unannotated-endpoints=true
|
||||
# quarkus.security.auth.proactive=true
|
||||
|
||||
# ============================================
|
||||
# Performance tuning PROD
|
||||
# Performance Tuning PROD
|
||||
# ============================================
|
||||
quarkus.thread-pool.core-threads=4
|
||||
quarkus.thread-pool.max-threads=32
|
||||
|
||||
@@ -1,108 +1,104 @@
|
||||
# ============================================================================
|
||||
# Lions User Manager - Server Implementation Configuration
|
||||
# Lions User Manager - Server Implementation Configuration (COMMUNE)
|
||||
# ============================================================================
|
||||
# Ce fichier contient UNIQUEMENT les propriétés COMMUNES à tous les environnements
|
||||
# Les propriétés spécifiques dev/prod vont dans application-dev.properties et application-prod.properties
|
||||
# ============================================================================
|
||||
|
||||
# Application Info
|
||||
# ============================================
|
||||
# Application Info (COMMUNE)
|
||||
# ============================================
|
||||
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
|
||||
# ============================================
|
||||
# OIDC Configuration (COMMUNE)
|
||||
# ============================================
|
||||
quarkus.oidc.application-type=service
|
||||
quarkus.oidc.discovery-enabled=true
|
||||
quarkus.oidc.roles.role-claim-path=realm_access/roles
|
||||
quarkus.oidc.token.audience=account
|
||||
|
||||
# Keycloak Admin Client Configuration
|
||||
lions.keycloak.server-url=https://security.lions.dev
|
||||
# ============================================
|
||||
# Keycloak Admin Client (COMMUNE)
|
||||
# ============================================
|
||||
lions.keycloak.admin-realm=master
|
||||
lions.keycloak.admin-client-id=admin-cli
|
||||
lions.keycloak.admin-username=${KEYCLOAK_ADMIN_USERNAME:admin}
|
||||
lions.keycloak.admin-password=${KEYCLOAK_ADMIN_PASSWORD:admin}
|
||||
lions.keycloak.connection-pool-size=10
|
||||
lions.keycloak.timeout-seconds=30
|
||||
|
||||
# Realms autorisés (séparés par virgule)
|
||||
lions.keycloak.authorized-realms=btpxpress,master,lions-realm
|
||||
# Quarkus-managed Keycloak Admin Client (uses Quarkus ObjectMapper with fail-on-unknown-properties=false)
|
||||
quarkus.keycloak.admin-client.realm=${lions.keycloak.admin-realm}
|
||||
quarkus.keycloak.admin-client.client-id=${lions.keycloak.admin-client-id}
|
||||
quarkus.keycloak.admin-client.grant-type=PASSWORD
|
||||
|
||||
# Circuit Breaker Configuration (SmallRye Fault Tolerance est activé par défaut)
|
||||
|
||||
# Retry Configuration (pour appels Keycloak)
|
||||
# ============================================
|
||||
# Retry Configuration (COMMUNE)
|
||||
# ============================================
|
||||
lions.keycloak.retry.max-attempts=3
|
||||
lions.keycloak.retry.delay-seconds=2
|
||||
|
||||
# Audit Configuration
|
||||
# ============================================
|
||||
# Audit Configuration (COMMUNE)
|
||||
# ============================================
|
||||
lions.audit.enabled=true
|
||||
lions.audit.log-to-database=false
|
||||
lions.audit.log-to-file=true
|
||||
lions.audit.retention-days=90
|
||||
|
||||
# Database Configuration (pour logs d'audit et données opérationnelles)
|
||||
# DÉSACTIVÉ - Non utilisé en production (logs gérés par Kubernetes)
|
||||
quarkus.datasource.health.enabled=false
|
||||
quarkus.datasource.devservices.enabled=false
|
||||
# ============================================
|
||||
# Database Configuration (COMMUNE)
|
||||
# ============================================
|
||||
quarkus.datasource.db-kind=postgresql
|
||||
quarkus.datasource.username=${DB_USERNAME:lions_user}
|
||||
quarkus.datasource.password=${DB_PASSWORD:lions_password}
|
||||
quarkus.datasource.jdbc.url=jdbc:postgresql://${DB_HOST:localhost}:${DB_PORT:5432}/${DB_NAME:lions_user_manager}
|
||||
quarkus.datasource.devservices.enabled=false
|
||||
|
||||
# Hibernate ORM Configuration
|
||||
quarkus.hibernate-orm.database.generation=update
|
||||
quarkus.hibernate-orm.log.sql=false
|
||||
# ============================================
|
||||
# Flyway Configuration (COMMUNE)
|
||||
# ============================================
|
||||
# Migration manuelle en production, vérifier avant d'activer
|
||||
|
||||
# Flyway Configuration
|
||||
# DÉSACTIVÉ - Pas de base de données en production
|
||||
quarkus.flyway.migrate-at-start=false
|
||||
|
||||
# Logging Configuration
|
||||
quarkus.log.level=INFO
|
||||
quarkus.log.category."dev.lions.user.manager".level=DEBUG
|
||||
quarkus.log.category."org.keycloak".level=WARN
|
||||
|
||||
quarkus.log.console.enable=true
|
||||
quarkus.log.console.format=%d{yyyy-MM-dd HH:mm:ss,SSS} %-5p [%c{3.}] (%t) %s%e%n
|
||||
|
||||
# File Logging pour Audit
|
||||
# DÉSACTIVÉ - Logs gérés par Kubernetes (stdout/stderr)
|
||||
quarkus.log.file.enable=false
|
||||
# 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)
|
||||
# ============================================
|
||||
# OpenAPI/Swagger Configuration (COMMUNE)
|
||||
# ============================================
|
||||
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
|
||||
# ============================================
|
||||
# Health Check Configuration (COMMUNE)
|
||||
# ============================================
|
||||
quarkus.smallrye-health.root-path=/health
|
||||
quarkus.smallrye-health.liveness-path=/health/live
|
||||
quarkus.smallrye-health.readiness-path=/health/ready
|
||||
|
||||
# Metrics Configuration
|
||||
# ============================================
|
||||
# Metrics Configuration (COMMUNE)
|
||||
# ============================================
|
||||
quarkus.micrometer.enabled=true
|
||||
quarkus.micrometer.export.prometheus.enabled=true
|
||||
quarkus.micrometer.export.prometheus.path=/metrics
|
||||
|
||||
# Security Configuration
|
||||
# ============================================
|
||||
# Security Configuration (COMMUNE)
|
||||
# ============================================
|
||||
quarkus.security.jaxrs.deny-unannotated-endpoints=false
|
||||
|
||||
# Jackson Configuration
|
||||
# ============================================
|
||||
# Jackson Configuration (COMMUNE)
|
||||
# ============================================
|
||||
quarkus.jackson.fail-on-unknown-properties=false
|
||||
quarkus.jackson.write-dates-as-timestamps=false
|
||||
quarkus.jackson.serialization-inclusion=non_null
|
||||
|
||||
# Dev Services (désactivé en production)
|
||||
quarkus.devservices.enabled=false
|
||||
# ============================================
|
||||
# Indexing (COMMUNE - pour 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
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
-- =============================================================================
|
||||
-- Migration Flyway V1.0.0 - Création de la table audit_logs
|
||||
-- =============================================================================
|
||||
-- Description: Création de la table pour la persistance des logs d'audit
|
||||
-- des actions effectuées sur le système de gestion des utilisateurs
|
||||
--
|
||||
-- Auteur: Lions Development Team
|
||||
-- Date: 2026-01-02
|
||||
-- Version: 1.0.0
|
||||
-- =============================================================================
|
||||
|
||||
-- Création de la table audit_logs
|
||||
CREATE TABLE IF NOT EXISTS audit_logs (
|
||||
-- Clé primaire générée automatiquement
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
|
||||
-- Informations sur l'utilisateur concerné
|
||||
user_id VARCHAR(255),
|
||||
|
||||
-- Type d'action effectuée
|
||||
action VARCHAR(100) NOT NULL,
|
||||
|
||||
-- Détails de l'action
|
||||
details TEXT,
|
||||
|
||||
-- Informations sur l'auteur de l'action
|
||||
auteur_action VARCHAR(255) NOT NULL,
|
||||
|
||||
-- Timestamp de l'action
|
||||
timestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
-- Informations de traçabilité réseau
|
||||
ip_address VARCHAR(45),
|
||||
user_agent VARCHAR(500),
|
||||
|
||||
-- Informations multi-tenant
|
||||
realm_name VARCHAR(255),
|
||||
|
||||
-- Statut de l'action
|
||||
success BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
error_message TEXT,
|
||||
|
||||
-- Métadonnées
|
||||
CONSTRAINT chk_audit_action CHECK (action IN (
|
||||
-- Actions utilisateurs
|
||||
'CREATION_UTILISATEUR',
|
||||
'MODIFICATION_UTILISATEUR',
|
||||
'SUPPRESSION_UTILISATEUR',
|
||||
'ACTIVATION_UTILISATEUR',
|
||||
'DESACTIVATION_UTILISATEUR',
|
||||
'VERROUILLAGE_UTILISATEUR',
|
||||
'DEVERROUILLAGE_UTILISATEUR',
|
||||
|
||||
-- Actions mot de passe
|
||||
'RESET_PASSWORD',
|
||||
'CHANGE_PASSWORD',
|
||||
'FORCE_PASSWORD_RESET',
|
||||
|
||||
-- Actions sessions
|
||||
'LOGOUT_UTILISATEUR',
|
||||
'LOGOUT_ALL_SESSIONS',
|
||||
'SESSION_EXPIREE',
|
||||
|
||||
-- Actions rôles
|
||||
'ATTRIBUTION_ROLE',
|
||||
'REVOCATION_ROLE',
|
||||
'CREATION_ROLE',
|
||||
'MODIFICATION_ROLE',
|
||||
'SUPPRESSION_ROLE',
|
||||
|
||||
-- Actions groupes
|
||||
'AJOUT_GROUPE',
|
||||
'RETRAIT_GROUPE',
|
||||
|
||||
-- Actions realms
|
||||
'ATTRIBUTION_REALM',
|
||||
'REVOCATION_REALM',
|
||||
|
||||
-- Actions synchronisation
|
||||
'SYNC_MANUEL',
|
||||
'SYNC_AUTO',
|
||||
'SYNC_ERREUR',
|
||||
|
||||
-- Actions import/export
|
||||
'EXPORT_CSV',
|
||||
'IMPORT_CSV',
|
||||
|
||||
-- Actions système
|
||||
'CONNEXION_REUSSIE',
|
||||
'CONNEXION_ECHOUEE',
|
||||
'TENTATIVE_ACCES_NON_AUTORISE',
|
||||
'ERREUR_SYSTEME',
|
||||
'CONFIGURATION_MODIFIEE'
|
||||
))
|
||||
);
|
||||
|
||||
-- =============================================================================
|
||||
-- INDEX pour optimiser les requêtes
|
||||
-- =============================================================================
|
||||
|
||||
-- Index sur user_id pour recherches rapides par utilisateur
|
||||
CREATE INDEX idx_audit_user_id ON audit_logs(user_id)
|
||||
WHERE user_id IS NOT NULL;
|
||||
|
||||
-- Index sur action pour filtrer par type d'action
|
||||
CREATE INDEX idx_audit_action ON audit_logs(action);
|
||||
|
||||
-- Index sur timestamp pour recherches chronologiques et tri
|
||||
CREATE INDEX idx_audit_timestamp ON audit_logs(timestamp DESC);
|
||||
|
||||
-- Index sur auteur_action pour tracer les actions d'un administrateur
|
||||
CREATE INDEX idx_audit_auteur ON audit_logs(auteur_action);
|
||||
|
||||
-- Index sur realm_name pour isolation multi-tenant
|
||||
CREATE INDEX idx_audit_realm ON audit_logs(realm_name)
|
||||
WHERE realm_name IS NOT NULL;
|
||||
|
||||
-- Index composite pour recherches fréquentes
|
||||
CREATE INDEX idx_audit_user_timestamp ON audit_logs(user_id, timestamp DESC)
|
||||
WHERE user_id IS NOT NULL;
|
||||
|
||||
-- Index sur success pour identifier rapidement les échecs
|
||||
CREATE INDEX idx_audit_failures ON audit_logs(success, timestamp DESC)
|
||||
WHERE success = FALSE;
|
||||
|
||||
-- =============================================================================
|
||||
-- COMMENTAIRES sur les colonnes
|
||||
-- =============================================================================
|
||||
|
||||
COMMENT ON TABLE audit_logs IS 'Table de persistance des logs d''audit pour traçabilité complète';
|
||||
|
||||
COMMENT ON COLUMN audit_logs.id IS 'Identifiant unique auto-incrémenté du log';
|
||||
COMMENT ON COLUMN audit_logs.user_id IS 'ID de l''utilisateur concerné par l''action (null pour actions système)';
|
||||
COMMENT ON COLUMN audit_logs.action IS 'Type d''action effectuée (enum TypeActionAudit)';
|
||||
COMMENT ON COLUMN audit_logs.details IS 'Détails complémentaires sur l''action';
|
||||
COMMENT ON COLUMN audit_logs.auteur_action IS 'Identifiant de l''utilisateur ayant effectué l''action';
|
||||
COMMENT ON COLUMN audit_logs.timestamp IS 'Date et heure précise de l''action';
|
||||
COMMENT ON COLUMN audit_logs.ip_address IS 'Adresse IP du client ayant effectué l''action';
|
||||
COMMENT ON COLUMN audit_logs.user_agent IS 'User-Agent du navigateur/client';
|
||||
COMMENT ON COLUMN audit_logs.realm_name IS 'Nom du realm Keycloak concerné (multi-tenant)';
|
||||
COMMENT ON COLUMN audit_logs.success IS 'Indique si l''action a réussi (true) ou échoué (false)';
|
||||
COMMENT ON COLUMN audit_logs.error_message IS 'Message d''erreur en cas d''échec (null si success=true)';
|
||||
|
||||
-- =============================================================================
|
||||
-- POLITIQUE DE RÉTENTION (optionnel - à activer selon besoins)
|
||||
-- =============================================================================
|
||||
|
||||
-- Fonction pour nettoyer automatiquement les vieux logs
|
||||
-- Décommenter et adapter la période de rétention selon les besoins
|
||||
|
||||
/*
|
||||
CREATE OR REPLACE FUNCTION cleanup_old_audit_logs() RETURNS void AS $$
|
||||
BEGIN
|
||||
-- Supprime les logs de plus de 365 jours (configurable)
|
||||
DELETE FROM audit_logs
|
||||
WHERE timestamp < CURRENT_TIMESTAMP - INTERVAL '365 days';
|
||||
|
||||
RAISE NOTICE 'Logs d''audit plus anciens que 365 jours supprimés';
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Créer un job CRON (nécessite extension pg_cron)
|
||||
-- SELECT cron.schedule('cleanup-audit-logs', '0 2 * * 0', 'SELECT cleanup_old_audit_logs()');
|
||||
*/
|
||||
|
||||
-- =============================================================================
|
||||
-- GRANTS (à adapter selon les rôles de votre base de données)
|
||||
-- =============================================================================
|
||||
|
||||
-- GRANT SELECT, INSERT ON audit_logs TO lions_app_user;
|
||||
-- GRANT USAGE, SELECT ON SEQUENCE audit_logs_id_seq TO lions_app_user;
|
||||
|
||||
-- =============================================================================
|
||||
-- FIN DE LA MIGRATION
|
||||
-- =============================================================================
|
||||
@@ -0,0 +1,85 @@
|
||||
-- =============================================================================
|
||||
-- Migration Flyway V2.0.0 - Création des tables de synchronisation Keycloak
|
||||
-- =============================================================================
|
||||
-- Description: Tables pour la persistance des snapshots et de l'historique
|
||||
-- des synchronisations entre l'application et Keycloak.
|
||||
--
|
||||
-- Entités correspondantes:
|
||||
-- SyncHistoryEntity → sync_history
|
||||
-- SyncedUserEntity → synced_user
|
||||
-- SyncedRoleEntity → synced_role
|
||||
--
|
||||
-- Auteur: Lions Development Team
|
||||
-- Date: 2026-02-17
|
||||
-- Version: 2.0.0
|
||||
-- =============================================================================
|
||||
|
||||
|
||||
-- =============================================================================
|
||||
-- TABLE sync_history : historique des opérations de synchronisation
|
||||
-- =============================================================================
|
||||
CREATE TABLE IF NOT EXISTS sync_history (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
realm_name VARCHAR(255) NOT NULL,
|
||||
sync_date TIMESTAMP NOT NULL,
|
||||
sync_type VARCHAR(50) NOT NULL, -- 'USER' ou 'ROLE'
|
||||
status VARCHAR(50) NOT NULL, -- 'SUCCESS' ou 'FAILURE'
|
||||
items_processed INTEGER,
|
||||
duration_ms BIGINT,
|
||||
error_message TEXT
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_sync_realm ON sync_history(realm_name);
|
||||
CREATE INDEX IF NOT EXISTS idx_sync_date ON sync_history(sync_date DESC);
|
||||
|
||||
COMMENT ON TABLE sync_history IS 'Historique des synchronisations Keycloak (users et rôles)';
|
||||
COMMENT ON COLUMN sync_history.sync_type IS 'Type de synchronisation : USER ou ROLE';
|
||||
COMMENT ON COLUMN sync_history.status IS 'Résultat : SUCCESS ou FAILURE';
|
||||
|
||||
|
||||
-- =============================================================================
|
||||
-- TABLE synced_user : snapshot local des utilisateurs Keycloak synchronisés
|
||||
-- =============================================================================
|
||||
CREATE TABLE IF NOT EXISTS synced_user (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
realm_name VARCHAR(255) NOT NULL,
|
||||
keycloak_id VARCHAR(255) NOT NULL,
|
||||
username VARCHAR(255) NOT NULL,
|
||||
email VARCHAR(255),
|
||||
enabled BOOLEAN,
|
||||
email_verified BOOLEAN,
|
||||
created_at TIMESTAMP,
|
||||
CONSTRAINT uq_synced_user_realm_kc UNIQUE (realm_name, keycloak_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_synced_user_realm
|
||||
ON synced_user(realm_name);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_synced_user_realm_kc_id
|
||||
ON synced_user(realm_name, keycloak_id);
|
||||
|
||||
COMMENT ON TABLE synced_user IS 'Snapshot local des utilisateurs Keycloak pour rapports et vérifications';
|
||||
COMMENT ON COLUMN synced_user.keycloak_id IS 'UUID Keycloak de l''utilisateur';
|
||||
|
||||
|
||||
-- =============================================================================
|
||||
-- TABLE synced_role : snapshot local des rôles Keycloak synchronisés
|
||||
-- =============================================================================
|
||||
CREATE TABLE IF NOT EXISTS synced_role (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
realm_name VARCHAR(255) NOT NULL,
|
||||
role_name VARCHAR(255) NOT NULL,
|
||||
description VARCHAR(500),
|
||||
CONSTRAINT uq_synced_role_realm_name UNIQUE (realm_name, role_name)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_synced_role_realm
|
||||
ON synced_role(realm_name);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_synced_role_realm_name
|
||||
ON synced_role(realm_name, role_name);
|
||||
|
||||
COMMENT ON TABLE synced_role IS 'Snapshot local des rôles Keycloak pour rapports et vérifications';
|
||||
COMMENT ON COLUMN synced_role.role_name IS 'Nom du rôle realm dans Keycloak';
|
||||
|
||||
-- =============================================================================
|
||||
-- FIN DE LA MIGRATION
|
||||
-- =============================================================================
|
||||
@@ -0,0 +1,163 @@
|
||||
package dev.lions.user.manager.client;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.keycloak.admin.client.Keycloak;
|
||||
import org.keycloak.admin.client.resource.RealmResource;
|
||||
import org.keycloak.admin.client.resource.RolesResource;
|
||||
import org.keycloak.admin.client.resource.UsersResource;
|
||||
import org.keycloak.admin.client.token.TokenManager;
|
||||
import org.keycloak.representations.info.ServerInfoRepresentation;
|
||||
import org.keycloak.admin.client.resource.ServerInfoResource;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import jakarta.ws.rs.NotFoundException;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
* Tests complets pour KeycloakAdminClientImpl
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class KeycloakAdminClientImplCompleteTest {
|
||||
|
||||
@Mock
|
||||
Keycloak mockKeycloak;
|
||||
|
||||
@InjectMocks
|
||||
KeycloakAdminClientImpl client;
|
||||
|
||||
private void setField(String fieldName, Object value) throws Exception {
|
||||
Field field = KeycloakAdminClientImpl.class.getDeclaredField(fieldName);
|
||||
field.setAccessible(true);
|
||||
field.set(client, value);
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws Exception {
|
||||
setField("serverUrl", "http://localhost:8180");
|
||||
setField("adminRealm", "master");
|
||||
setField("adminClientId", "admin-cli");
|
||||
setField("adminUsername", "admin");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetInstance() {
|
||||
Keycloak result = client.getInstance();
|
||||
assertSame(mockKeycloak, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetRealm_Success() {
|
||||
RealmResource mockRealmResource = mock(RealmResource.class);
|
||||
when(mockKeycloak.realm("test-realm")).thenReturn(mockRealmResource);
|
||||
|
||||
RealmResource result = client.getRealm("test-realm");
|
||||
assertSame(mockRealmResource, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetRealm_Exception() {
|
||||
when(mockKeycloak.realm("bad-realm")).thenThrow(new RuntimeException("Connection error"));
|
||||
|
||||
assertThrows(RuntimeException.class, () -> client.getRealm("bad-realm"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetUsers() {
|
||||
RealmResource mockRealmResource = mock(RealmResource.class);
|
||||
UsersResource mockUsersResource = mock(UsersResource.class);
|
||||
when(mockKeycloak.realm("test-realm")).thenReturn(mockRealmResource);
|
||||
when(mockRealmResource.users()).thenReturn(mockUsersResource);
|
||||
|
||||
UsersResource result = client.getUsers("test-realm");
|
||||
assertSame(mockUsersResource, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetRoles() {
|
||||
RealmResource mockRealmResource = mock(RealmResource.class);
|
||||
RolesResource mockRolesResource = mock(RolesResource.class);
|
||||
when(mockKeycloak.realm("test-realm")).thenReturn(mockRealmResource);
|
||||
when(mockRealmResource.roles()).thenReturn(mockRolesResource);
|
||||
|
||||
RolesResource result = client.getRoles("test-realm");
|
||||
assertSame(mockRolesResource, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testIsConnected_True() {
|
||||
ServerInfoResource mockServerInfoResource = mock(ServerInfoResource.class);
|
||||
when(mockKeycloak.serverInfo()).thenReturn(mockServerInfoResource);
|
||||
when(mockServerInfoResource.getInfo()).thenReturn(mock(ServerInfoRepresentation.class));
|
||||
|
||||
assertTrue(client.isConnected());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testIsConnected_False() {
|
||||
when(mockKeycloak.serverInfo()).thenThrow(new RuntimeException("Connection refused"));
|
||||
|
||||
assertFalse(client.isConnected());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRealmExists_True() {
|
||||
RealmResource mockRealmResource = mock(RealmResource.class);
|
||||
RolesResource mockRolesResource = mock(RolesResource.class);
|
||||
when(mockKeycloak.realm("test-realm")).thenReturn(mockRealmResource);
|
||||
when(mockRealmResource.roles()).thenReturn(mockRolesResource);
|
||||
when(mockRolesResource.list()).thenReturn(List.of());
|
||||
|
||||
assertTrue(client.realmExists("test-realm"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRealmExists_NotFound() {
|
||||
RealmResource mockRealmResource = mock(RealmResource.class);
|
||||
RolesResource mockRolesResource = mock(RolesResource.class);
|
||||
when(mockKeycloak.realm("missing")).thenReturn(mockRealmResource);
|
||||
when(mockRealmResource.roles()).thenReturn(mockRolesResource);
|
||||
when(mockRolesResource.list()).thenThrow(new NotFoundException("Not found"));
|
||||
|
||||
assertFalse(client.realmExists("missing"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRealmExists_OtherException() {
|
||||
when(mockKeycloak.realm("error-realm")).thenThrow(new RuntimeException("Other error"));
|
||||
|
||||
assertTrue(client.realmExists("error-realm"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetAllRealms_TokenError() {
|
||||
// When token retrieval fails, getAllRealms should throw
|
||||
when(mockKeycloak.tokenManager()).thenThrow(new RuntimeException("Token error"));
|
||||
|
||||
assertThrows(RuntimeException.class, () -> client.getAllRealms());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetAllRealms_NullTokenManager() {
|
||||
when(mockKeycloak.tokenManager()).thenReturn(null);
|
||||
|
||||
assertThrows(RuntimeException.class, () -> client.getAllRealms());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testClose() {
|
||||
assertDoesNotThrow(() -> client.close());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testReconnect() {
|
||||
assertDoesNotThrow(() -> client.reconnect());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
package dev.lions.user.manager.client;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.keycloak.admin.client.Keycloak;
|
||||
import org.keycloak.admin.client.resource.RealmResource;
|
||||
import org.keycloak.admin.client.resource.RolesResource;
|
||||
import org.keycloak.admin.client.resource.ServerInfoResource;
|
||||
import org.keycloak.admin.client.resource.UsersResource;
|
||||
import org.keycloak.representations.info.ServerInfoRepresentation;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import jakarta.ws.rs.NotFoundException;
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class KeycloakAdminClientImplTest {
|
||||
|
||||
@InjectMocks
|
||||
KeycloakAdminClientImpl client;
|
||||
|
||||
@Mock
|
||||
Keycloak keycloak;
|
||||
|
||||
@Mock
|
||||
RealmResource realmResource;
|
||||
|
||||
@Mock
|
||||
UsersResource usersResource;
|
||||
|
||||
@Mock
|
||||
RolesResource rolesResource;
|
||||
|
||||
@Mock
|
||||
ServerInfoResource serverInfoResource;
|
||||
|
||||
private void setField(Object target, String fieldName, Object value) throws Exception {
|
||||
Field field = target.getClass().getDeclaredField(fieldName);
|
||||
field.setAccessible(true);
|
||||
field.set(target, value);
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws Exception {
|
||||
setField(client, "serverUrl", "http://localhost:8180");
|
||||
setField(client, "adminRealm", "master");
|
||||
setField(client, "adminClientId", "admin-cli");
|
||||
setField(client, "adminUsername", "admin");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetInstance() {
|
||||
Keycloak result = client.getInstance();
|
||||
assertNotNull(result);
|
||||
assertEquals(keycloak, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetRealm() {
|
||||
when(keycloak.realm("test-realm")).thenReturn(realmResource);
|
||||
|
||||
RealmResource result = client.getRealm("test-realm");
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals(realmResource, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetRealmThrowsException() {
|
||||
when(keycloak.realm("bad-realm")).thenThrow(new RuntimeException("Connection failed"));
|
||||
|
||||
assertThrows(RuntimeException.class, () -> client.getRealm("bad-realm"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetUsers() {
|
||||
when(keycloak.realm("test-realm")).thenReturn(realmResource);
|
||||
when(realmResource.users()).thenReturn(usersResource);
|
||||
|
||||
UsersResource result = client.getUsers("test-realm");
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals(usersResource, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetRoles() {
|
||||
when(keycloak.realm("test-realm")).thenReturn(realmResource);
|
||||
when(realmResource.roles()).thenReturn(rolesResource);
|
||||
|
||||
RolesResource result = client.getRoles("test-realm");
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals(rolesResource, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testIsConnected_true() {
|
||||
when(keycloak.serverInfo()).thenReturn(serverInfoResource);
|
||||
when(serverInfoResource.getInfo()).thenReturn(new ServerInfoRepresentation());
|
||||
|
||||
assertTrue(client.isConnected());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testIsConnected_false_exception() {
|
||||
when(keycloak.serverInfo()).thenThrow(new RuntimeException("Connection refused"));
|
||||
|
||||
assertFalse(client.isConnected());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRealmExists_true() {
|
||||
when(keycloak.realm("test-realm")).thenReturn(realmResource);
|
||||
when(realmResource.roles()).thenReturn(rolesResource);
|
||||
when(rolesResource.list()).thenReturn(java.util.Collections.emptyList());
|
||||
|
||||
assertTrue(client.realmExists("test-realm"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRealmExists_notFound() {
|
||||
when(keycloak.realm("missing-realm")).thenReturn(realmResource);
|
||||
when(realmResource.roles()).thenReturn(rolesResource);
|
||||
when(rolesResource.list()).thenThrow(new NotFoundException("Realm not found"));
|
||||
|
||||
assertFalse(client.realmExists("missing-realm"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRealmExists_otherException() {
|
||||
when(keycloak.realm("problem-realm")).thenReturn(realmResource);
|
||||
when(realmResource.roles()).thenReturn(rolesResource);
|
||||
when(rolesResource.list()).thenThrow(new RuntimeException("Some other error"));
|
||||
|
||||
assertTrue(client.realmExists("problem-realm"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testClose() {
|
||||
assertDoesNotThrow(() -> client.close());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testReconnect() {
|
||||
assertDoesNotThrow(() -> client.reconnect());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,356 @@
|
||||
package dev.lions.user.manager.config;
|
||||
|
||||
import io.quarkus.runtime.StartupEvent;
|
||||
import jakarta.ws.rs.NotFoundException;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.keycloak.admin.client.Keycloak;
|
||||
import org.keycloak.admin.client.KeycloakBuilder;
|
||||
import org.keycloak.admin.client.resource.*;
|
||||
import org.keycloak.representations.idm.*;
|
||||
import org.mockito.MockedStatic;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.net.URI;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
* Tests complets pour KeycloakTestUserConfig pour atteindre 100% de couverture
|
||||
* Teste toutes les méthodes privées via la méthode publique onStart
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class KeycloakTestUserConfigCompleteTest {
|
||||
|
||||
private KeycloakTestUserConfig config;
|
||||
private Keycloak adminClient;
|
||||
private RealmsResource realmsResource;
|
||||
private RealmResource realmResource;
|
||||
private RolesResource rolesResource;
|
||||
private RoleResource roleResource;
|
||||
private UsersResource usersResource;
|
||||
private UserResource userResource;
|
||||
private ClientsResource clientsResource;
|
||||
private ClientResource clientResource;
|
||||
private ClientScopesResource clientScopesResource;
|
||||
private ClientScopeResource clientScopeResource;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws Exception {
|
||||
config = new KeycloakTestUserConfig();
|
||||
|
||||
// Injecter les valeurs via reflection
|
||||
setField("profile", "dev");
|
||||
setField("keycloakServerUrl", "http://localhost:8080");
|
||||
setField("adminRealm", "master");
|
||||
setField("adminUsername", "admin");
|
||||
setField("adminPassword", "admin");
|
||||
setField("authorizedRealms", "lions-user-manager");
|
||||
|
||||
// Mocks pour Keycloak
|
||||
adminClient = mock(Keycloak.class);
|
||||
realmsResource = mock(RealmsResource.class);
|
||||
realmResource = mock(RealmResource.class);
|
||||
rolesResource = mock(RolesResource.class);
|
||||
roleResource = mock(RoleResource.class);
|
||||
usersResource = mock(UsersResource.class);
|
||||
userResource = mock(UserResource.class);
|
||||
clientsResource = mock(ClientsResource.class);
|
||||
clientResource = mock(ClientResource.class);
|
||||
clientScopesResource = mock(ClientScopesResource.class);
|
||||
clientScopeResource = mock(ClientScopeResource.class);
|
||||
}
|
||||
|
||||
private void setField(String fieldName, Object value) throws Exception {
|
||||
java.lang.reflect.Field field = KeycloakTestUserConfig.class.getDeclaredField(fieldName);
|
||||
field.setAccessible(true);
|
||||
field.set(config, value);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testOnStart_DevMode() {
|
||||
// Le code est désactivé, donc onStart devrait juste logger et retourner
|
||||
StartupEvent event = mock(StartupEvent.class);
|
||||
|
||||
// Ne devrait pas lancer d'exception
|
||||
assertDoesNotThrow(() -> config.onStart(event));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEnsureRealmExists_RealmExists() throws Exception {
|
||||
when(adminClient.realms()).thenReturn(realmsResource);
|
||||
when(realmsResource.realm("lions-user-manager")).thenReturn(realmResource);
|
||||
when(realmResource.toRepresentation()).thenReturn(new RealmRepresentation());
|
||||
|
||||
Method method = KeycloakTestUserConfig.class.getDeclaredMethod("ensureRealmExists", Keycloak.class);
|
||||
method.setAccessible(true);
|
||||
|
||||
assertDoesNotThrow(() -> method.invoke(config, adminClient));
|
||||
verify(realmResource).toRepresentation();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEnsureRealmExists_RealmNotFound() throws Exception {
|
||||
when(adminClient.realms()).thenReturn(realmsResource);
|
||||
when(realmsResource.realm("lions-user-manager")).thenReturn(realmResource);
|
||||
when(realmResource.toRepresentation()).thenThrow(new NotFoundException());
|
||||
doNothing().when(realmsResource).create(any(RealmRepresentation.class));
|
||||
|
||||
Method method = KeycloakTestUserConfig.class.getDeclaredMethod("ensureRealmExists", Keycloak.class);
|
||||
method.setAccessible(true);
|
||||
|
||||
assertDoesNotThrow(() -> method.invoke(config, adminClient));
|
||||
verify(realmResource).toRepresentation();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEnsureRolesExist_AllRolesExist() throws Exception {
|
||||
when(adminClient.realms()).thenReturn(realmsResource);
|
||||
when(realmsResource.realm("lions-user-manager")).thenReturn(realmResource);
|
||||
when(realmResource.roles()).thenReturn(rolesResource);
|
||||
when(rolesResource.get(anyString())).thenReturn(roleResource);
|
||||
when(roleResource.toRepresentation()).thenReturn(new RoleRepresentation());
|
||||
|
||||
Method method = KeycloakTestUserConfig.class.getDeclaredMethod("ensureRolesExist", Keycloak.class);
|
||||
method.setAccessible(true);
|
||||
|
||||
assertDoesNotThrow(() -> method.invoke(config, adminClient));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEnsureRolesExist_RoleNotFound() throws Exception {
|
||||
when(adminClient.realms()).thenReturn(realmsResource);
|
||||
when(realmsResource.realm("lions-user-manager")).thenReturn(realmResource);
|
||||
when(realmResource.roles()).thenReturn(rolesResource);
|
||||
when(rolesResource.get(anyString())).thenReturn(roleResource);
|
||||
when(roleResource.toRepresentation())
|
||||
.thenThrow(new NotFoundException())
|
||||
.thenReturn(new RoleRepresentation());
|
||||
doNothing().when(rolesResource).create(any(RoleRepresentation.class));
|
||||
|
||||
Method method = KeycloakTestUserConfig.class.getDeclaredMethod("ensureRolesExist", Keycloak.class);
|
||||
method.setAccessible(true);
|
||||
|
||||
assertDoesNotThrow(() -> method.invoke(config, adminClient));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEnsureTestUserExists_UserExists() throws Exception {
|
||||
when(adminClient.realms()).thenReturn(realmsResource);
|
||||
when(realmsResource.realm("lions-user-manager")).thenReturn(realmResource);
|
||||
when(realmResource.users()).thenReturn(usersResource);
|
||||
|
||||
UserRepresentation existingUser = new UserRepresentation();
|
||||
existingUser.setId("user-id-123");
|
||||
when(usersResource.search("test-user", true)).thenReturn(List.of(existingUser));
|
||||
|
||||
Method method = KeycloakTestUserConfig.class.getDeclaredMethod("ensureTestUserExists", Keycloak.class);
|
||||
method.setAccessible(true);
|
||||
|
||||
String userId = (String) method.invoke(config, adminClient);
|
||||
assertEquals("user-id-123", userId);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEnsureTestUserExists_UserNotFound() throws Exception {
|
||||
when(adminClient.realms()).thenReturn(realmsResource);
|
||||
when(realmsResource.realm("lions-user-manager")).thenReturn(realmResource);
|
||||
when(realmResource.users()).thenReturn(usersResource);
|
||||
when(usersResource.search("test-user", true)).thenReturn(Collections.emptyList());
|
||||
|
||||
Response response = mock(Response.class);
|
||||
when(response.getStatusInfo()).thenReturn(Response.Status.CREATED);
|
||||
when(response.getLocation()).thenReturn(URI.create("http://localhost/users/user-id-123"));
|
||||
when(usersResource.create(any(UserRepresentation.class))).thenReturn(response);
|
||||
when(usersResource.get("user-id-123")).thenReturn(userResource);
|
||||
|
||||
CredentialRepresentation credential = new CredentialRepresentation();
|
||||
doNothing().when(userResource).resetPassword(any(CredentialRepresentation.class));
|
||||
|
||||
Method method = KeycloakTestUserConfig.class.getDeclaredMethod("ensureTestUserExists", Keycloak.class);
|
||||
method.setAccessible(true);
|
||||
|
||||
String userId = (String) method.invoke(config, adminClient);
|
||||
assertEquals("user-id-123", userId);
|
||||
verify(usersResource).create(any(UserRepresentation.class));
|
||||
verify(userResource).resetPassword(any(CredentialRepresentation.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAssignRolesToUser() throws Exception {
|
||||
when(adminClient.realms()).thenReturn(realmsResource);
|
||||
when(realmsResource.realm("lions-user-manager")).thenReturn(realmResource);
|
||||
when(realmResource.users()).thenReturn(usersResource);
|
||||
when(realmResource.roles()).thenReturn(rolesResource);
|
||||
when(rolesResource.get(anyString())).thenReturn(roleResource);
|
||||
|
||||
RoleRepresentation role = new RoleRepresentation();
|
||||
role.setName("admin");
|
||||
when(roleResource.toRepresentation()).thenReturn(role);
|
||||
|
||||
when(usersResource.get("user-id")).thenReturn(userResource);
|
||||
RoleMappingResource roleMappingResource = mock(RoleMappingResource.class);
|
||||
RoleScopeResource roleScopeResource = mock(RoleScopeResource.class);
|
||||
when(userResource.roles()).thenReturn(roleMappingResource);
|
||||
when(roleMappingResource.realmLevel()).thenReturn(roleScopeResource);
|
||||
doNothing().when(roleScopeResource).add(anyList());
|
||||
|
||||
Method method = KeycloakTestUserConfig.class.getDeclaredMethod("assignRolesToUser", Keycloak.class, String.class);
|
||||
method.setAccessible(true);
|
||||
|
||||
assertDoesNotThrow(() -> method.invoke(config, adminClient, "user-id"));
|
||||
verify(roleScopeResource).add(anyList());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEnsureClientAndMapper_ClientExists() throws Exception {
|
||||
when(adminClient.realms()).thenReturn(realmsResource);
|
||||
when(realmsResource.realm("lions-user-manager")).thenReturn(realmResource);
|
||||
when(realmResource.clients()).thenReturn(clientsResource);
|
||||
|
||||
ClientRepresentation existingClient = new ClientRepresentation();
|
||||
existingClient.setId("client-id-123");
|
||||
when(clientsResource.findByClientId("lions-user-manager-client")).thenReturn(List.of(existingClient));
|
||||
|
||||
when(realmResource.clientScopes()).thenReturn(clientScopesResource);
|
||||
ClientScopeRepresentation rolesScope = new ClientScopeRepresentation();
|
||||
rolesScope.setId("scope-id");
|
||||
rolesScope.setName("roles");
|
||||
when(clientScopesResource.findAll()).thenReturn(List.of(rolesScope));
|
||||
|
||||
when(clientsResource.get("client-id-123")).thenReturn(clientResource);
|
||||
when(clientResource.getDefaultClientScopes()).thenReturn(List.of(rolesScope));
|
||||
|
||||
Method method = KeycloakTestUserConfig.class.getDeclaredMethod("ensureClientAndMapper", Keycloak.class);
|
||||
method.setAccessible(true);
|
||||
|
||||
assertDoesNotThrow(() -> method.invoke(config, adminClient));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEnsureClientAndMapper_ClientNotFound() throws Exception {
|
||||
when(adminClient.realms()).thenReturn(realmsResource);
|
||||
when(realmsResource.realm("lions-user-manager")).thenReturn(realmResource);
|
||||
when(realmResource.clients()).thenReturn(clientsResource);
|
||||
when(clientsResource.findByClientId("lions-user-manager-client")).thenReturn(Collections.emptyList());
|
||||
|
||||
Response response = mock(Response.class);
|
||||
when(response.getStatusInfo()).thenReturn(Response.Status.CREATED);
|
||||
when(response.getLocation()).thenReturn(URI.create("http://localhost/clients/client-id-123"));
|
||||
when(clientsResource.create(any(ClientRepresentation.class))).thenReturn(response);
|
||||
|
||||
when(realmResource.clientScopes()).thenReturn(clientScopesResource);
|
||||
ClientScopeRepresentation rolesScope = new ClientScopeRepresentation();
|
||||
rolesScope.setId("scope-id");
|
||||
rolesScope.setName("roles");
|
||||
when(clientScopesResource.findAll()).thenReturn(List.of(rolesScope));
|
||||
|
||||
when(clientsResource.get("client-id-123")).thenReturn(clientResource);
|
||||
when(clientResource.getDefaultClientScopes()).thenReturn(List.of(rolesScope));
|
||||
|
||||
Method method = KeycloakTestUserConfig.class.getDeclaredMethod("ensureClientAndMapper", Keycloak.class);
|
||||
method.setAccessible(true);
|
||||
|
||||
assertDoesNotThrow(() -> method.invoke(config, adminClient));
|
||||
verify(clientsResource).create(any(ClientRepresentation.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEnsureClientAndMapper_ClientNotFound_NoRolesScope() throws Exception {
|
||||
when(adminClient.realms()).thenReturn(realmsResource);
|
||||
when(realmsResource.realm("lions-user-manager")).thenReturn(realmResource);
|
||||
when(realmResource.clients()).thenReturn(clientsResource);
|
||||
when(clientsResource.findByClientId("lions-user-manager-client")).thenReturn(Collections.emptyList());
|
||||
|
||||
Response response = mock(Response.class);
|
||||
when(response.getStatusInfo()).thenReturn(Response.Status.CREATED);
|
||||
when(response.getLocation()).thenReturn(URI.create("http://localhost/clients/client-id-123"));
|
||||
when(clientsResource.create(any(ClientRepresentation.class))).thenReturn(response);
|
||||
|
||||
when(realmResource.clientScopes()).thenReturn(clientScopesResource);
|
||||
when(clientScopesResource.findAll()).thenReturn(Collections.emptyList());
|
||||
|
||||
Method method = KeycloakTestUserConfig.class.getDeclaredMethod("ensureClientAndMapper", Keycloak.class);
|
||||
method.setAccessible(true);
|
||||
|
||||
assertDoesNotThrow(() -> method.invoke(config, adminClient));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEnsureClientAndMapper_ClientNotFound_RolesScopeAlreadyPresent() throws Exception {
|
||||
when(adminClient.realms()).thenReturn(realmsResource);
|
||||
when(realmsResource.realm("lions-user-manager")).thenReturn(realmResource);
|
||||
when(realmResource.clients()).thenReturn(clientsResource);
|
||||
when(clientsResource.findByClientId("lions-user-manager-client")).thenReturn(Collections.emptyList());
|
||||
|
||||
Response response = mock(Response.class);
|
||||
when(response.getStatusInfo()).thenReturn(Response.Status.CREATED);
|
||||
when(response.getLocation()).thenReturn(URI.create("http://localhost/clients/client-id-123"));
|
||||
when(clientsResource.create(any(ClientRepresentation.class))).thenReturn(response);
|
||||
|
||||
when(realmResource.clientScopes()).thenReturn(clientScopesResource);
|
||||
ClientScopeRepresentation rolesScope = new ClientScopeRepresentation();
|
||||
rolesScope.setId("scope-id");
|
||||
rolesScope.setName("roles");
|
||||
when(clientScopesResource.findAll()).thenReturn(List.of(rolesScope));
|
||||
|
||||
when(clientsResource.get("client-id-123")).thenReturn(clientResource);
|
||||
// Simuler que le scope "roles" est déjà présent
|
||||
when(clientResource.getDefaultClientScopes()).thenReturn(List.of(rolesScope));
|
||||
|
||||
Method method = KeycloakTestUserConfig.class.getDeclaredMethod("ensureClientAndMapper", Keycloak.class);
|
||||
method.setAccessible(true);
|
||||
|
||||
assertDoesNotThrow(() -> method.invoke(config, adminClient));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEnsureClientAndMapper_Exception() throws Exception {
|
||||
when(adminClient.realms()).thenReturn(realmsResource);
|
||||
when(realmsResource.realm("lions-user-manager")).thenReturn(realmResource);
|
||||
when(realmResource.clients()).thenReturn(clientsResource);
|
||||
when(clientsResource.findByClientId("lions-user-manager-client")).thenThrow(new RuntimeException("Connection error"));
|
||||
|
||||
Method method = KeycloakTestUserConfig.class.getDeclaredMethod("ensureClientAndMapper", Keycloak.class);
|
||||
method.setAccessible(true);
|
||||
|
||||
assertDoesNotThrow(() -> method.invoke(config, adminClient));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetCreatedId_Success() throws Exception {
|
||||
Response response = mock(Response.class);
|
||||
when(response.getStatusInfo()).thenReturn(Response.Status.CREATED);
|
||||
when(response.getLocation()).thenReturn(URI.create("http://localhost/users/user-id-123"));
|
||||
|
||||
Method method = KeycloakTestUserConfig.class.getDeclaredMethod("getCreatedId", Response.class);
|
||||
method.setAccessible(true);
|
||||
|
||||
String id = (String) method.invoke(config, response);
|
||||
assertEquals("user-id-123", id);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetCreatedId_Error() throws Exception {
|
||||
Response response = mock(Response.class);
|
||||
// Utiliser Response.Status.BAD_REQUEST directement
|
||||
when(response.getStatusInfo()).thenReturn(Response.Status.BAD_REQUEST);
|
||||
|
||||
Method method = KeycloakTestUserConfig.class.getDeclaredMethod("getCreatedId", Response.class);
|
||||
method.setAccessible(true);
|
||||
|
||||
Exception exception = assertThrows(Exception.class, () -> method.invoke(config, response));
|
||||
assertTrue(exception.getCause() instanceof RuntimeException);
|
||||
assertTrue(exception.getCause().getMessage().contains("Erreur lors de la création"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
package dev.lions.user.manager.config;
|
||||
|
||||
import io.quarkus.runtime.StartupEvent;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Tests unitaires pour KeycloakTestUserConfig
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class KeycloakTestUserConfigTest {
|
||||
|
||||
@InjectMocks
|
||||
private KeycloakTestUserConfig config;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws Exception {
|
||||
// Injecter les propriétés via reflection
|
||||
setField("profile", "dev");
|
||||
setField("keycloakServerUrl", "http://localhost:8180");
|
||||
setField("adminRealm", "master");
|
||||
setField("adminUsername", "admin");
|
||||
setField("adminPassword", "admin");
|
||||
setField("authorizedRealms", "lions-user-manager");
|
||||
}
|
||||
|
||||
private void setField(String fieldName, Object value) throws Exception {
|
||||
Field field = KeycloakTestUserConfig.class.getDeclaredField(fieldName);
|
||||
field.setAccessible(true);
|
||||
field.set(config, value);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testOnStart_DevMode() {
|
||||
// La méthode onStart est désactivée, elle devrait juste logger et retourner
|
||||
assertDoesNotThrow(() -> {
|
||||
config.onStart(new StartupEvent());
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void testOnStart_ProdMode() throws Exception {
|
||||
setField("profile", "prod");
|
||||
|
||||
// En prod, la méthode devrait retourner immédiatement
|
||||
assertDoesNotThrow(() -> {
|
||||
config.onStart(new StartupEvent());
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConstants() {
|
||||
// Vérifier que les constantes sont définies
|
||||
assertNotNull(KeycloakTestUserConfig.class);
|
||||
// Les constantes sont privées, on ne peut pas les tester directement
|
||||
// mais on peut vérifier que la classe se charge correctement
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package dev.lions.user.manager.mapper;
|
||||
|
||||
import dev.lions.user.manager.dto.role.RoleDTO;
|
||||
import dev.lions.user.manager.enums.role.TypeRole;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.keycloak.representations.idm.RoleRepresentation;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Tests supplémentaires pour RoleMapper pour améliorer la couverture
|
||||
*/
|
||||
class RoleMapperAdditionalTest {
|
||||
|
||||
@Test
|
||||
void testToDTO_WithAllFields() {
|
||||
RoleRepresentation roleRep = new RoleRepresentation();
|
||||
roleRep.setId("role-123");
|
||||
roleRep.setName("admin");
|
||||
roleRep.setDescription("Administrator role");
|
||||
roleRep.setComposite(false);
|
||||
|
||||
RoleDTO dto = RoleMapper.toDTO(roleRep, "test-realm", TypeRole.REALM_ROLE);
|
||||
|
||||
assertNotNull(dto);
|
||||
assertEquals("role-123", dto.getId());
|
||||
assertEquals("admin", dto.getName());
|
||||
assertEquals("Administrator role", dto.getDescription());
|
||||
assertEquals(TypeRole.REALM_ROLE, dto.getTypeRole());
|
||||
assertFalse(dto.getComposite());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testToDTO_WithNullFields() {
|
||||
RoleRepresentation roleRep = new RoleRepresentation();
|
||||
roleRep.setId("role-123");
|
||||
roleRep.setName("user");
|
||||
|
||||
RoleDTO dto = RoleMapper.toDTO(roleRep, "test-realm", TypeRole.REALM_ROLE);
|
||||
|
||||
assertNotNull(dto);
|
||||
assertEquals("role-123", dto.getId());
|
||||
assertEquals("user", dto.getName());
|
||||
assertNull(dto.getDescription());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testToDTOList_Empty() {
|
||||
List<RoleDTO> dtos = RoleMapper.toDTOList(Collections.emptyList(), "test-realm", TypeRole.REALM_ROLE);
|
||||
|
||||
assertNotNull(dtos);
|
||||
assertTrue(dtos.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testToDTOList_WithRoles() {
|
||||
RoleRepresentation role1 = new RoleRepresentation();
|
||||
role1.setId("role-1");
|
||||
role1.setName("admin");
|
||||
RoleRepresentation role2 = new RoleRepresentation();
|
||||
role2.setId("role-2");
|
||||
role2.setName("user");
|
||||
|
||||
List<RoleDTO> dtos = RoleMapper.toDTOList(Arrays.asList(role1, role2), "test-realm", TypeRole.REALM_ROLE);
|
||||
|
||||
assertNotNull(dtos);
|
||||
assertEquals(2, dtos.size());
|
||||
assertEquals("admin", dtos.get(0).getName());
|
||||
assertEquals("user", dtos.get(1).getName());
|
||||
}
|
||||
|
||||
// La méthode toKeycloak() n'existe pas dans RoleMapper
|
||||
// Ces tests sont supprimés car la méthode n'est pas disponible
|
||||
}
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
package dev.lions.user.manager.mapper;
|
||||
|
||||
import dev.lions.user.manager.dto.role.RoleDTO;
|
||||
import dev.lions.user.manager.enums.role.TypeRole;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.keycloak.representations.idm.RoleRepresentation;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class RoleMapperTest {
|
||||
|
||||
@Test
|
||||
void testToDTO() {
|
||||
RoleRepresentation rep = new RoleRepresentation();
|
||||
rep.setId("1");
|
||||
rep.setName("role");
|
||||
rep.setDescription("desc");
|
||||
rep.setComposite(true);
|
||||
|
||||
RoleDTO dto = RoleMapper.toDTO(rep, "realm", TypeRole.REALM_ROLE);
|
||||
|
||||
assertNotNull(dto);
|
||||
assertEquals("1", dto.getId());
|
||||
assertEquals("role", dto.getName());
|
||||
assertEquals("desc", dto.getDescription());
|
||||
assertEquals(TypeRole.REALM_ROLE, dto.getTypeRole());
|
||||
assertEquals("realm", dto.getRealmName());
|
||||
assertTrue(dto.getComposite());
|
||||
|
||||
assertNull(RoleMapper.toDTO(null, "realm", TypeRole.REALM_ROLE));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testToRepresentation() {
|
||||
RoleDTO dto = RoleDTO.builder()
|
||||
.id("1")
|
||||
.name("role")
|
||||
.description("desc")
|
||||
.composite(true)
|
||||
.compositeRoles(Collections.singletonList("subrole"))
|
||||
.typeRole(TypeRole.CLIENT_ROLE) // Should setClientRole(true)
|
||||
.build();
|
||||
|
||||
RoleRepresentation rep = RoleMapper.toRepresentation(dto);
|
||||
|
||||
assertNotNull(rep);
|
||||
assertEquals("1", rep.getId());
|
||||
assertEquals("role", rep.getName());
|
||||
assertEquals("desc", rep.getDescription());
|
||||
assertTrue(rep.isComposite());
|
||||
assertTrue(rep.getClientRole());
|
||||
|
||||
assertNull(RoleMapper.toRepresentation(null));
|
||||
}
|
||||
|
||||
// New test case to cover full branch logic
|
||||
@Test
|
||||
void testToRepresentationRealmRole() {
|
||||
RoleDTO dto = RoleDTO.builder()
|
||||
.typeRole(TypeRole.REALM_ROLE)
|
||||
.build();
|
||||
RoleRepresentation rep = RoleMapper.toRepresentation(dto);
|
||||
assertFalse(rep.getClientRole());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testToDTOList() {
|
||||
RoleRepresentation rep = new RoleRepresentation();
|
||||
rep.setName("role");
|
||||
List<RoleRepresentation> reps = Collections.singletonList(rep);
|
||||
|
||||
List<RoleDTO> dtos = RoleMapper.toDTOList(reps, "realm", TypeRole.REALM_ROLE);
|
||||
assertEquals(1, dtos.size());
|
||||
assertEquals("role", dtos.get(0).getName());
|
||||
|
||||
assertTrue(RoleMapper.toDTOList(null, "realm", TypeRole.REALM_ROLE).isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testToRepresentationList() {
|
||||
RoleDTO dto = RoleDTO.builder().name("role").typeRole(TypeRole.REALM_ROLE).build();
|
||||
List<RoleDTO> dtos = Collections.singletonList(dto);
|
||||
|
||||
List<RoleRepresentation> reps = RoleMapper.toRepresentationList(dtos);
|
||||
assertEquals(1, reps.size());
|
||||
assertEquals("role", reps.get(0).getName());
|
||||
|
||||
assertTrue(RoleMapper.toRepresentationList(null).isEmpty());
|
||||
}
|
||||
}
|
||||
150
src/test/java/dev/lions/user/manager/mapper/UserMapperTest.java
Normal file
150
src/test/java/dev/lions/user/manager/mapper/UserMapperTest.java
Normal file
@@ -0,0 +1,150 @@
|
||||
package dev.lions.user.manager.mapper;
|
||||
|
||||
import dev.lions.user.manager.dto.user.UserDTO;
|
||||
import dev.lions.user.manager.enums.user.StatutUser;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class UserMapperTest {
|
||||
|
||||
@Test
|
||||
void testToDTO() {
|
||||
UserRepresentation rep = new UserRepresentation();
|
||||
rep.setId("1");
|
||||
rep.setUsername("jdoe");
|
||||
rep.setEmail("jdoe@example.com");
|
||||
rep.setEmailVerified(true);
|
||||
rep.setFirstName("John");
|
||||
rep.setLastName("Doe");
|
||||
rep.setEnabled(true);
|
||||
rep.setCreatedTimestamp(System.currentTimeMillis());
|
||||
|
||||
Map<String, List<String>> attrs = Map.of(
|
||||
"phone_number", List.of("123"),
|
||||
"organization", List.of("Lions"),
|
||||
"department", List.of("IT"),
|
||||
"job_title", List.of("Dev"),
|
||||
"country", List.of("CI"),
|
||||
"city", List.of("Abidjan"),
|
||||
"locale", List.of("fr"),
|
||||
"timezone", List.of("UTC"));
|
||||
rep.setAttributes(attrs);
|
||||
|
||||
UserDTO dto = UserMapper.toDTO(rep, "realm");
|
||||
|
||||
assertNotNull(dto);
|
||||
assertEquals("1", dto.getId());
|
||||
assertEquals("jdoe", dto.getUsername());
|
||||
assertEquals("jdoe@example.com", dto.getEmail());
|
||||
assertTrue(dto.getEmailVerified());
|
||||
assertEquals("John", dto.getPrenom());
|
||||
assertEquals("Doe", dto.getNom());
|
||||
assertEquals(StatutUser.ACTIF, dto.getStatut());
|
||||
assertEquals("realm", dto.getRealmName());
|
||||
assertEquals("123", dto.getTelephone());
|
||||
assertEquals("Lions", dto.getOrganisation());
|
||||
assertEquals("IT", dto.getDepartement());
|
||||
assertEquals("Dev", dto.getFonction());
|
||||
assertEquals("CI", dto.getPays());
|
||||
assertEquals("Abidjan", dto.getVille());
|
||||
assertEquals("fr", dto.getLangue());
|
||||
assertEquals("UTC", dto.getTimezone());
|
||||
assertNotNull(dto.getDateCreation());
|
||||
|
||||
assertNull(UserMapper.toDTO(null, "realm"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testToDTOWithNullAttributes() {
|
||||
UserRepresentation rep = new UserRepresentation();
|
||||
rep.setId("1");
|
||||
rep.setEnabled(true);
|
||||
UserDTO dto = UserMapper.toDTO(rep, "realm");
|
||||
assertNotNull(dto);
|
||||
assertNull(dto.getTelephone()); // Attribute missing
|
||||
}
|
||||
|
||||
@Test
|
||||
void testToDTOWithEmptyAttributes() {
|
||||
UserRepresentation rep = new UserRepresentation();
|
||||
rep.setEnabled(true);
|
||||
rep.setAttributes(Collections.emptyMap());
|
||||
UserDTO dto = UserMapper.toDTO(rep, "realm");
|
||||
assertNotNull(dto);
|
||||
assertNull(dto.getTelephone());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testToRepresentation() {
|
||||
UserDTO dto = UserDTO.builder()
|
||||
.id("1")
|
||||
.username("jdoe")
|
||||
.email("jdoe@example.com")
|
||||
.emailVerified(true)
|
||||
.prenom("John")
|
||||
.nom("Doe")
|
||||
.enabled(true)
|
||||
.telephone("123")
|
||||
.organisation("Lions")
|
||||
.departement("IT")
|
||||
.fonction("Dev")
|
||||
.pays("CI")
|
||||
.ville("Abidjan")
|
||||
.langue("fr")
|
||||
.timezone("UTC")
|
||||
.requiredActions(Collections.singletonList("UPDATE_PASSWORD"))
|
||||
.attributes(Map.of("custom", List.of("value")))
|
||||
.build();
|
||||
|
||||
UserRepresentation rep = UserMapper.toRepresentation(dto);
|
||||
|
||||
assertNotNull(rep);
|
||||
assertEquals("1", rep.getId());
|
||||
assertEquals("jdoe", rep.getUsername());
|
||||
assertEquals("jdoe@example.com", rep.getEmail());
|
||||
assertTrue(rep.isEmailVerified());
|
||||
assertEquals("John", rep.getFirstName());
|
||||
assertEquals("Doe", rep.getLastName());
|
||||
assertTrue(rep.isEnabled());
|
||||
|
||||
assertNotNull(rep.getAttributes());
|
||||
assertEquals(List.of("123"), rep.getAttributes().get("phone_number"));
|
||||
assertEquals(List.of("Lions"), rep.getAttributes().get("organization"));
|
||||
assertEquals(List.of("value"), rep.getAttributes().get("custom"));
|
||||
|
||||
assertNotNull(rep.getRequiredActions());
|
||||
assertTrue(rep.getRequiredActions().contains("UPDATE_PASSWORD"));
|
||||
|
||||
assertNull(UserMapper.toRepresentation(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testToRepresentationValuesNull() {
|
||||
UserDTO dto = UserDTO.builder().username("jdoe").enabled(null).build();
|
||||
UserRepresentation rep = UserMapper.toRepresentation(dto);
|
||||
assertTrue(rep.isEnabled()); // Defaults to true in mapper
|
||||
}
|
||||
|
||||
@Test
|
||||
void testToDTOList() {
|
||||
UserRepresentation rep = new UserRepresentation();
|
||||
rep.setEnabled(true);
|
||||
List<UserRepresentation> reps = Collections.singletonList(rep);
|
||||
List<UserDTO> dtos = UserMapper.toDTOList(reps, "realm");
|
||||
assertEquals(1, dtos.size());
|
||||
|
||||
assertTrue(UserMapper.toDTOList(null, "realm").isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPrivateConstructor() throws Exception {
|
||||
java.lang.reflect.Constructor<UserMapper> constructor = UserMapper.class.getDeclaredConstructor();
|
||||
assertTrue(java.lang.reflect.Modifier.isPrivate(constructor.getModifiers()));
|
||||
constructor.setAccessible(true);
|
||||
assertNotNull(constructor.newInstance());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
package dev.lions.user.manager.resource;
|
||||
|
||||
import dev.lions.user.manager.dto.audit.AuditLogDTO;
|
||||
import dev.lions.user.manager.dto.common.CountDTO;
|
||||
import dev.lions.user.manager.enums.audit.TypeActionAudit;
|
||||
import dev.lions.user.manager.service.AuditService;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class AuditResourceTest {
|
||||
|
||||
@Mock
|
||||
AuditService auditService;
|
||||
|
||||
@InjectMocks
|
||||
AuditResource auditResource;
|
||||
|
||||
@Test
|
||||
void testSearchLogs() {
|
||||
List<AuditLogDTO> logs = Collections.singletonList(
|
||||
AuditLogDTO.builder().acteurUsername("admin").typeAction(TypeActionAudit.USER_CREATE).build());
|
||||
when(auditService.findByActeur(eq("admin"), any(), any(), eq(0), eq(50))).thenReturn(logs);
|
||||
|
||||
List<AuditLogDTO> result = auditResource.searchLogs("admin", null, null, null, null, null, 0, 50);
|
||||
|
||||
assertEquals(logs, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetLogsByActor() {
|
||||
List<AuditLogDTO> logs = Collections.singletonList(
|
||||
AuditLogDTO.builder().acteurUsername("admin").build());
|
||||
when(auditService.findByActeur(eq("admin"), isNull(), isNull(), eq(0), eq(100))).thenReturn(logs);
|
||||
|
||||
List<AuditLogDTO> result = auditResource.getLogsByActor("admin", 100);
|
||||
|
||||
assertEquals(logs, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetLogsByResource() {
|
||||
List<AuditLogDTO> logs = Collections.emptyList();
|
||||
when(auditService.findByRessource(eq("USER"), eq("1"), any(), any(), eq(0), eq(100)))
|
||||
.thenReturn(logs);
|
||||
|
||||
List<AuditLogDTO> result = auditResource.getLogsByResource("USER", "1", 100);
|
||||
|
||||
assertEquals(logs, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetLogsByAction() {
|
||||
List<AuditLogDTO> logs = Collections.emptyList();
|
||||
when(auditService.findByTypeAction(eq(TypeActionAudit.USER_CREATE), eq("master"), any(), any(), eq(0), eq(100)))
|
||||
.thenReturn(logs);
|
||||
|
||||
List<AuditLogDTO> result = auditResource.getLogsByAction(TypeActionAudit.USER_CREATE, null, null, 100);
|
||||
|
||||
assertEquals(logs, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetActionStatistics() {
|
||||
Map<TypeActionAudit, Long> stats = Map.of(TypeActionAudit.USER_CREATE, 10L);
|
||||
when(auditService.countByActionType(eq("master"), any(), any())).thenReturn(stats);
|
||||
|
||||
Map<TypeActionAudit, Long> result = auditResource.getActionStatistics(null, null);
|
||||
|
||||
assertEquals(stats, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetUserActivityStatistics() {
|
||||
Map<String, Long> stats = Map.of("admin", 100L);
|
||||
when(auditService.countByActeur(eq("master"), any(), any())).thenReturn(stats);
|
||||
|
||||
Map<String, Long> result = auditResource.getUserActivityStatistics(null, null);
|
||||
|
||||
assertEquals(stats, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetFailureCount() {
|
||||
Map<String, Long> successVsFailure = Map.of("failure", 5L, "success", 100L);
|
||||
when(auditService.countSuccessVsFailure(eq("master"), any(), any())).thenReturn(successVsFailure);
|
||||
|
||||
CountDTO result = auditResource.getFailureCount(null, null);
|
||||
|
||||
assertEquals(5L, result.getCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetSuccessCount() {
|
||||
Map<String, Long> successVsFailure = Map.of("failure", 5L, "success", 100L);
|
||||
when(auditService.countSuccessVsFailure(eq("master"), any(), any())).thenReturn(successVsFailure);
|
||||
|
||||
CountDTO result = auditResource.getSuccessCount(null, null);
|
||||
|
||||
assertEquals(100L, result.getCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testExportLogsToCSV() {
|
||||
when(auditService.exportToCSV(eq("master"), any(), any())).thenReturn("csv,data");
|
||||
|
||||
Response response = auditResource.exportLogsToCSV(null, null);
|
||||
|
||||
assertEquals(200, response.getStatus());
|
||||
assertEquals("csv,data", response.getEntity());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPurgeOldLogs() {
|
||||
doNothing().when(auditService).purgeOldLogs(any());
|
||||
|
||||
auditResource.purgeOldLogs(90);
|
||||
|
||||
verify(auditService).purgeOldLogs(any());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package dev.lions.user.manager.resource;
|
||||
|
||||
import dev.lions.user.manager.client.KeycloakAdminClient;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.keycloak.admin.client.Keycloak;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class HealthResourceEndpointTest {
|
||||
|
||||
@Mock
|
||||
KeycloakAdminClient keycloakAdminClient;
|
||||
|
||||
@Mock
|
||||
Keycloak keycloak;
|
||||
|
||||
@InjectMocks
|
||||
HealthResourceEndpoint healthResourceEndpoint;
|
||||
|
||||
@Test
|
||||
void testGetKeycloakHealthConnected() {
|
||||
when(keycloakAdminClient.getInstance()).thenReturn(keycloak);
|
||||
|
||||
Map<String, Object> result = healthResourceEndpoint.getKeycloakHealth();
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals("UP", result.get("status"));
|
||||
assertEquals(true, result.get("connected"));
|
||||
assertNotNull(result.get("timestamp"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetKeycloakHealthDisconnected() {
|
||||
when(keycloakAdminClient.getInstance()).thenReturn(null);
|
||||
|
||||
Map<String, Object> result = healthResourceEndpoint.getKeycloakHealth();
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals("DOWN", result.get("status"));
|
||||
assertEquals(false, result.get("connected"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetKeycloakHealthError() {
|
||||
when(keycloakAdminClient.getInstance()).thenThrow(new RuntimeException("Connection error"));
|
||||
|
||||
Map<String, Object> result = healthResourceEndpoint.getKeycloakHealth();
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals("ERROR", result.get("status"));
|
||||
assertEquals(false, result.get("connected"));
|
||||
assertEquals("Connection error", result.get("error"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetServiceStatusConnected() {
|
||||
when(keycloakAdminClient.isConnected()).thenReturn(true);
|
||||
|
||||
Map<String, Object> result = healthResourceEndpoint.getServiceStatus();
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals("lions-user-manager-server", result.get("service"));
|
||||
assertEquals("1.0.0", result.get("version"));
|
||||
assertEquals("UP", result.get("status"));
|
||||
assertEquals("CONNECTED", result.get("keycloak"));
|
||||
assertNotNull(result.get("timestamp"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetServiceStatusDisconnected() {
|
||||
when(keycloakAdminClient.isConnected()).thenReturn(false);
|
||||
|
||||
Map<String, Object> result = healthResourceEndpoint.getServiceStatus();
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals("UP", result.get("status"));
|
||||
assertEquals("DISCONNECTED", result.get("keycloak"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetServiceStatusKeycloakError() {
|
||||
when(keycloakAdminClient.isConnected()).thenThrow(new RuntimeException("Error"));
|
||||
|
||||
Map<String, Object> result = healthResourceEndpoint.getServiceStatus();
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals("UP", result.get("status"));
|
||||
assertEquals("ERROR", result.get("keycloak"));
|
||||
assertEquals("Error", result.get("keycloakError"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
package dev.lions.user.manager.resource;
|
||||
|
||||
import dev.lions.user.manager.dto.realm.AuthorizedRealmsDTO;
|
||||
import dev.lions.user.manager.dto.realm.RealmAccessCheckDTO;
|
||||
import dev.lions.user.manager.dto.realm.RealmAssignmentDTO;
|
||||
import dev.lions.user.manager.service.RealmAuthorizationService;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import jakarta.ws.rs.core.SecurityContext;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
* Tests unitaires pour RealmAssignmentResource
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class RealmAssignmentResourceTest {
|
||||
|
||||
@Mock
|
||||
private RealmAuthorizationService realmAuthorizationService;
|
||||
|
||||
@Mock
|
||||
private SecurityContext securityContext;
|
||||
|
||||
@Mock
|
||||
private Principal principal;
|
||||
|
||||
@InjectMocks
|
||||
private RealmAssignmentResource realmAssignmentResource;
|
||||
|
||||
private RealmAssignmentDTO assignment;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
assignment = RealmAssignmentDTO.builder()
|
||||
.id("assignment-1")
|
||||
.userId("user-1")
|
||||
.username("testuser")
|
||||
.email("test@example.com")
|
||||
.realmName("realm1")
|
||||
.isSuperAdmin(false)
|
||||
.active(true)
|
||||
.assignedAt(LocalDateTime.now())
|
||||
.assignedBy("admin")
|
||||
.build();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetAllAssignments_Success() {
|
||||
List<RealmAssignmentDTO> assignments = Arrays.asList(assignment);
|
||||
when(realmAuthorizationService.getAllAssignments()).thenReturn(assignments);
|
||||
|
||||
List<RealmAssignmentDTO> result = realmAssignmentResource.getAllAssignments();
|
||||
|
||||
assertEquals(1, result.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetAssignmentsByUser_Success() {
|
||||
List<RealmAssignmentDTO> assignments = Arrays.asList(assignment);
|
||||
when(realmAuthorizationService.getAssignmentsByUser("user-1")).thenReturn(assignments);
|
||||
|
||||
List<RealmAssignmentDTO> result = realmAssignmentResource.getAssignmentsByUser("user-1");
|
||||
|
||||
assertEquals(1, result.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetAssignmentsByRealm_Success() {
|
||||
List<RealmAssignmentDTO> assignments = Arrays.asList(assignment);
|
||||
when(realmAuthorizationService.getAssignmentsByRealm("realm1")).thenReturn(assignments);
|
||||
|
||||
List<RealmAssignmentDTO> result = realmAssignmentResource.getAssignmentsByRealm("realm1");
|
||||
|
||||
assertEquals(1, result.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetAssignmentById_Success() {
|
||||
when(realmAuthorizationService.getAssignmentById("assignment-1")).thenReturn(Optional.of(assignment));
|
||||
|
||||
RealmAssignmentDTO result = realmAssignmentResource.getAssignmentById("assignment-1");
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals("assignment-1", result.getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetAssignmentById_NotFound() {
|
||||
when(realmAuthorizationService.getAssignmentById("non-existent")).thenReturn(Optional.empty());
|
||||
|
||||
assertThrows(RuntimeException.class, () -> realmAssignmentResource.getAssignmentById("non-existent"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCanManageRealm_Success() {
|
||||
when(realmAuthorizationService.canManageRealm("user-1", "realm1")).thenReturn(true);
|
||||
|
||||
RealmAccessCheckDTO result = realmAssignmentResource.canManageRealm("user-1", "realm1");
|
||||
|
||||
assertTrue(result.isCanManage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetAuthorizedRealms_Success() {
|
||||
List<String> realms = Arrays.asList("realm1", "realm2");
|
||||
when(realmAuthorizationService.getAuthorizedRealms("user-1")).thenReturn(realms);
|
||||
when(realmAuthorizationService.isSuperAdmin("user-1")).thenReturn(false);
|
||||
|
||||
AuthorizedRealmsDTO result = realmAssignmentResource.getAuthorizedRealms("user-1");
|
||||
|
||||
assertEquals(2, result.getRealms().size());
|
||||
assertFalse(result.isSuperAdmin());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAssignRealmToUser_Success() {
|
||||
// En Quarkus, @Context SecurityContext injecté peut être simulé via Mockito
|
||||
// Mais dans RealmAssignmentResource, l'admin est récupéré du SecurityContext.
|
||||
// Puisque c'est un test unitaire @ExtendWith(MockitoExtension.class),
|
||||
// @Inject SecurityContext securityContext est mocké.
|
||||
|
||||
when(securityContext.getUserPrincipal()).thenReturn(principal);
|
||||
when(principal.getName()).thenReturn("admin");
|
||||
when(realmAuthorizationService.assignRealmToUser(any(RealmAssignmentDTO.class))).thenReturn(assignment);
|
||||
|
||||
Response response = realmAssignmentResource.assignRealmToUser(assignment);
|
||||
|
||||
assertEquals(201, response.getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRevokeRealmFromUser_Success() {
|
||||
doNothing().when(realmAuthorizationService).revokeRealmFromUser("user-1", "realm1");
|
||||
|
||||
realmAssignmentResource.revokeRealmFromUser("user-1", "realm1");
|
||||
|
||||
verify(realmAuthorizationService).revokeRealmFromUser("user-1", "realm1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRevokeAllRealmsFromUser_Success() {
|
||||
doNothing().when(realmAuthorizationService).revokeAllRealmsFromUser("user-1");
|
||||
|
||||
realmAssignmentResource.revokeAllRealmsFromUser("user-1");
|
||||
|
||||
verify(realmAuthorizationService).revokeAllRealmsFromUser("user-1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDeactivateAssignment_Success() {
|
||||
doNothing().when(realmAuthorizationService).deactivateAssignment("assignment-1");
|
||||
|
||||
realmAssignmentResource.deactivateAssignment("assignment-1");
|
||||
|
||||
verify(realmAuthorizationService).deactivateAssignment("assignment-1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testActivateAssignment_Success() {
|
||||
doNothing().when(realmAuthorizationService).activateAssignment("assignment-1");
|
||||
|
||||
realmAssignmentResource.activateAssignment("assignment-1");
|
||||
|
||||
verify(realmAuthorizationService).activateAssignment("assignment-1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSetSuperAdmin_Success() {
|
||||
doNothing().when(realmAuthorizationService).setSuperAdmin("user-1", true);
|
||||
|
||||
realmAssignmentResource.setSuperAdmin("user-1", true);
|
||||
|
||||
verify(realmAuthorizationService).setSuperAdmin("user-1", true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package dev.lions.user.manager.resource;
|
||||
|
||||
import dev.lions.user.manager.client.KeycloakAdminClient;
|
||||
import io.quarkus.security.identity.SecurityIdentity;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
* Tests supplémentaires pour RealmResource pour améliorer la couverture
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class RealmResourceAdditionalTest {
|
||||
|
||||
@Mock
|
||||
private KeycloakAdminClient keycloakAdminClient;
|
||||
|
||||
@Mock
|
||||
private SecurityIdentity securityIdentity;
|
||||
|
||||
@InjectMocks
|
||||
private RealmResource realmResource;
|
||||
|
||||
@Test
|
||||
void testGetAllRealms_Success() {
|
||||
List<String> realms = Arrays.asList("master", "lions-user-manager", "test-realm");
|
||||
when(keycloakAdminClient.getAllRealms()).thenReturn(realms);
|
||||
|
||||
List<String> result = realmResource.getAllRealms();
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals(3, result.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetAllRealms_Empty() {
|
||||
when(keycloakAdminClient.getAllRealms()).thenReturn(List.of());
|
||||
|
||||
List<String> result = realmResource.getAllRealms();
|
||||
|
||||
assertNotNull(result);
|
||||
assertTrue(result.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetAllRealms_Exception() {
|
||||
when(keycloakAdminClient.getAllRealms()).thenThrow(new RuntimeException("Connection error"));
|
||||
|
||||
assertThrows(RuntimeException.class, () -> realmResource.getAllRealms());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package dev.lions.user.manager.resource;
|
||||
|
||||
import dev.lions.user.manager.client.KeycloakAdminClient;
|
||||
import io.quarkus.security.identity.SecurityIdentity;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
* Tests unitaires pour RealmResource
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class RealmResourceTest {
|
||||
|
||||
@Mock
|
||||
private KeycloakAdminClient keycloakAdminClient;
|
||||
|
||||
@Mock
|
||||
private SecurityIdentity securityIdentity;
|
||||
|
||||
@InjectMocks
|
||||
private RealmResource realmResource;
|
||||
|
||||
@Test
|
||||
void testGetAllRealms_Success() {
|
||||
List<String> realms = Arrays.asList("master", "lions-user-manager", "btpxpress");
|
||||
when(keycloakAdminClient.getAllRealms()).thenReturn(realms);
|
||||
|
||||
List<String> result = realmResource.getAllRealms();
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals(3, result.size());
|
||||
assertEquals("master", result.get(0));
|
||||
verify(keycloakAdminClient).getAllRealms();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetAllRealms_EmptyList() {
|
||||
when(keycloakAdminClient.getAllRealms()).thenReturn(Collections.emptyList());
|
||||
|
||||
List<String> result = realmResource.getAllRealms();
|
||||
|
||||
assertNotNull(result);
|
||||
assertTrue(result.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetAllRealms_Exception() {
|
||||
when(keycloakAdminClient.getAllRealms()).thenThrow(new RuntimeException("Keycloak connection error"));
|
||||
|
||||
assertThrows(RuntimeException.class, () -> realmResource.getAllRealms());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
package dev.lions.user.manager.resource;
|
||||
|
||||
import dev.lions.user.manager.dto.role.RoleAssignmentDTO;
|
||||
import dev.lions.user.manager.dto.role.RoleAssignmentRequestDTO;
|
||||
import dev.lions.user.manager.dto.role.RoleDTO;
|
||||
import dev.lions.user.manager.enums.role.TypeRole;
|
||||
import dev.lions.user.manager.service.RoleService;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class RoleResourceTest {
|
||||
|
||||
@Mock
|
||||
RoleService roleService;
|
||||
|
||||
@InjectMocks
|
||||
RoleResource roleResource;
|
||||
|
||||
private static final String REALM = "test-realm";
|
||||
private static final String CLIENT_ID = "test-client";
|
||||
|
||||
// ============== Realm Role Tests ==============
|
||||
|
||||
@Test
|
||||
void testCreateRealmRole() {
|
||||
RoleDTO input = RoleDTO.builder().name("role").description("desc").build();
|
||||
RoleDTO created = RoleDTO.builder().id("1").name("role").description("desc").build();
|
||||
|
||||
when(roleService.createRealmRole(any(), eq(REALM))).thenReturn(created);
|
||||
|
||||
Response response = roleResource.createRealmRole(input, REALM);
|
||||
|
||||
assertEquals(201, response.getStatus());
|
||||
assertEquals(created, response.getEntity());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreateRealmRoleConflict() {
|
||||
RoleDTO input = RoleDTO.builder().name("role").build();
|
||||
|
||||
when(roleService.createRealmRole(any(), eq(REALM)))
|
||||
.thenThrow(new IllegalArgumentException("Role already exists"));
|
||||
|
||||
Response response = roleResource.createRealmRole(input, REALM);
|
||||
|
||||
assertEquals(409, response.getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetRealmRole() {
|
||||
RoleDTO role = RoleDTO.builder().id("1").name("role").build();
|
||||
when(roleService.getRoleByName("role", REALM, TypeRole.REALM_ROLE, null))
|
||||
.thenReturn(Optional.of(role));
|
||||
|
||||
RoleDTO result = roleResource.getRealmRole("role", REALM);
|
||||
|
||||
assertEquals(role, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetRealmRoleNotFound() {
|
||||
when(roleService.getRoleByName("role", REALM, TypeRole.REALM_ROLE, null))
|
||||
.thenReturn(Optional.empty());
|
||||
|
||||
assertThrows(RuntimeException.class, () -> roleResource.getRealmRole("role", REALM));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetAllRealmRoles() {
|
||||
List<RoleDTO> roles = Collections.singletonList(RoleDTO.builder().name("role").build());
|
||||
when(roleService.getAllRealmRoles(REALM)).thenReturn(roles);
|
||||
|
||||
List<RoleDTO> result = roleResource.getAllRealmRoles(REALM);
|
||||
|
||||
assertEquals(roles, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testUpdateRealmRole() {
|
||||
RoleDTO existingRole = RoleDTO.builder().id("1").name("role").build();
|
||||
RoleDTO input = RoleDTO.builder().description("updated").build();
|
||||
RoleDTO updated = RoleDTO.builder().id("1").name("role").description("updated").build();
|
||||
|
||||
when(roleService.getRoleByName("role", REALM, TypeRole.REALM_ROLE, null))
|
||||
.thenReturn(Optional.of(existingRole));
|
||||
when(roleService.updateRole(eq("1"), any(), eq(REALM), eq(TypeRole.REALM_ROLE), isNull()))
|
||||
.thenReturn(updated);
|
||||
|
||||
RoleDTO result = roleResource.updateRealmRole("role", input, REALM);
|
||||
|
||||
assertEquals(updated, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDeleteRealmRole() {
|
||||
RoleDTO existingRole = RoleDTO.builder().id("1").name("role").build();
|
||||
when(roleService.getRoleByName("role", REALM, TypeRole.REALM_ROLE, null))
|
||||
.thenReturn(Optional.of(existingRole));
|
||||
doNothing().when(roleService).deleteRole(eq("1"), eq(REALM), eq(TypeRole.REALM_ROLE), isNull());
|
||||
|
||||
roleResource.deleteRealmRole("role", REALM);
|
||||
|
||||
verify(roleService).deleteRole(eq("1"), eq(REALM), eq(TypeRole.REALM_ROLE), isNull());
|
||||
}
|
||||
|
||||
// ============== Client Role Tests ==============
|
||||
|
||||
@Test
|
||||
void testCreateClientRole() {
|
||||
RoleDTO input = RoleDTO.builder().name("role").build();
|
||||
RoleDTO created = RoleDTO.builder().id("1").name("role").build();
|
||||
|
||||
when(roleService.createClientRole(any(RoleDTO.class), eq(CLIENT_ID), eq(REALM))).thenReturn(created);
|
||||
|
||||
Response response = roleResource.createClientRole(CLIENT_ID, input, REALM);
|
||||
|
||||
assertEquals(201, response.getStatus());
|
||||
assertEquals(created, response.getEntity());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetClientRole() {
|
||||
RoleDTO role = RoleDTO.builder().id("1").name("role").build();
|
||||
when(roleService.getRoleByName("role", REALM, TypeRole.CLIENT_ROLE, CLIENT_ID))
|
||||
.thenReturn(Optional.of(role));
|
||||
|
||||
RoleDTO result = roleResource.getClientRole(CLIENT_ID, "role", REALM);
|
||||
|
||||
assertEquals(role, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetAllClientRoles() {
|
||||
List<RoleDTO> roles = Collections.singletonList(RoleDTO.builder().name("role").build());
|
||||
when(roleService.getAllClientRoles(REALM, CLIENT_ID)).thenReturn(roles);
|
||||
|
||||
List<RoleDTO> result = roleResource.getAllClientRoles(CLIENT_ID, REALM);
|
||||
|
||||
assertEquals(roles, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDeleteClientRole() {
|
||||
RoleDTO existingRole = RoleDTO.builder().id("1").name("role").build();
|
||||
when(roleService.getRoleByName("role", REALM, TypeRole.CLIENT_ROLE, CLIENT_ID))
|
||||
.thenReturn(Optional.of(existingRole));
|
||||
doNothing().when(roleService).deleteRole(eq("1"), eq(REALM), eq(TypeRole.CLIENT_ROLE), eq(CLIENT_ID));
|
||||
|
||||
roleResource.deleteClientRole(CLIENT_ID, "role", REALM);
|
||||
|
||||
verify(roleService).deleteRole(eq("1"), eq(REALM), eq(TypeRole.CLIENT_ROLE), eq(CLIENT_ID));
|
||||
}
|
||||
|
||||
// ============== Role Assignment Tests ==============
|
||||
|
||||
@Test
|
||||
void testAssignRealmRoles() {
|
||||
doNothing().when(roleService).assignRolesToUser(any(RoleAssignmentDTO.class));
|
||||
|
||||
RoleAssignmentRequestDTO request = RoleAssignmentRequestDTO.builder()
|
||||
.roleNames(Collections.singletonList("role"))
|
||||
.build();
|
||||
|
||||
roleResource.assignRealmRoles("user1", REALM, request);
|
||||
|
||||
verify(roleService).assignRolesToUser(any(RoleAssignmentDTO.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRevokeRealmRoles() {
|
||||
doNothing().when(roleService).revokeRolesFromUser(any(RoleAssignmentDTO.class));
|
||||
|
||||
RoleAssignmentRequestDTO request = RoleAssignmentRequestDTO.builder()
|
||||
.roleNames(Collections.singletonList("role"))
|
||||
.build();
|
||||
|
||||
roleResource.revokeRealmRoles("user1", REALM, request);
|
||||
|
||||
verify(roleService).revokeRolesFromUser(any(RoleAssignmentDTO.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAssignClientRoles() {
|
||||
doNothing().when(roleService).assignRolesToUser(any(RoleAssignmentDTO.class));
|
||||
|
||||
RoleAssignmentRequestDTO request = RoleAssignmentRequestDTO.builder()
|
||||
.roleNames(Collections.singletonList("role"))
|
||||
.build();
|
||||
|
||||
roleResource.assignClientRoles(CLIENT_ID, "user1", REALM, request);
|
||||
|
||||
verify(roleService).assignRolesToUser(any(RoleAssignmentDTO.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetUserRealmRoles() {
|
||||
List<RoleDTO> roles = Collections.singletonList(RoleDTO.builder().name("role").build());
|
||||
when(roleService.getUserRealmRoles("user1", REALM)).thenReturn(roles);
|
||||
|
||||
List<RoleDTO> result = roleResource.getUserRealmRoles("user1", REALM);
|
||||
|
||||
assertEquals(roles, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetUserClientRoles() {
|
||||
List<RoleDTO> roles = Collections.singletonList(RoleDTO.builder().name("role").build());
|
||||
when(roleService.getUserClientRoles("user1", CLIENT_ID, REALM)).thenReturn(roles);
|
||||
|
||||
List<RoleDTO> result = roleResource.getUserClientRoles(CLIENT_ID, "user1", REALM);
|
||||
|
||||
assertEquals(roles, result);
|
||||
}
|
||||
|
||||
// ============== Composite Role Tests ==============
|
||||
|
||||
@Test
|
||||
void testAddComposites() {
|
||||
RoleDTO parentRole = RoleDTO.builder().id("parent-1").name("role").build();
|
||||
RoleDTO childRole = RoleDTO.builder().id("child-1").name("composite").build();
|
||||
|
||||
when(roleService.getRoleByName("role", REALM, TypeRole.REALM_ROLE, null))
|
||||
.thenReturn(Optional.of(parentRole));
|
||||
when(roleService.getRoleByName("composite", REALM, TypeRole.REALM_ROLE, null))
|
||||
.thenReturn(Optional.of(childRole));
|
||||
doNothing().when(roleService).addCompositeRoles(eq("parent-1"), anyList(), eq(REALM),
|
||||
eq(TypeRole.REALM_ROLE), isNull());
|
||||
|
||||
RoleAssignmentRequestDTO request = RoleAssignmentRequestDTO.builder()
|
||||
.roleNames(Collections.singletonList("composite"))
|
||||
.build();
|
||||
|
||||
roleResource.addComposites("role", REALM, request);
|
||||
|
||||
verify(roleService).addCompositeRoles(eq("parent-1"), anyList(), eq(REALM),
|
||||
eq(TypeRole.REALM_ROLE), isNull());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetComposites() {
|
||||
RoleDTO role = RoleDTO.builder().id("1").name("role").build();
|
||||
List<RoleDTO> composites = Collections.singletonList(RoleDTO.builder().name("composite").build());
|
||||
|
||||
when(roleService.getRoleByName("role", REALM, TypeRole.REALM_ROLE, null))
|
||||
.thenReturn(Optional.of(role));
|
||||
when(roleService.getCompositeRoles("1", REALM, TypeRole.REALM_ROLE, null))
|
||||
.thenReturn(composites);
|
||||
|
||||
List<RoleDTO> result = roleResource.getComposites("role", REALM);
|
||||
|
||||
assertEquals(composites, result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package dev.lions.user.manager.resource;
|
||||
|
||||
import dev.lions.user.manager.api.SyncResourceApi;
|
||||
import dev.lions.user.manager.dto.sync.HealthStatusDTO;
|
||||
import dev.lions.user.manager.dto.sync.SyncResultDTO;
|
||||
import dev.lions.user.manager.service.SyncService;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class SyncResourceTest {
|
||||
|
||||
@Mock
|
||||
SyncService syncService;
|
||||
|
||||
@InjectMocks
|
||||
SyncResource syncResource;
|
||||
|
||||
private static final String REALM = "test-realm";
|
||||
|
||||
@Test
|
||||
void testCheckKeycloakHealth() {
|
||||
when(syncService.isKeycloakAvailable()).thenReturn(true);
|
||||
when(syncService.getKeycloakHealthInfo()).thenReturn(Map.of("version", "23.0.0"));
|
||||
|
||||
HealthStatusDTO status = syncResource.checkKeycloakHealth();
|
||||
|
||||
assertTrue(status.isKeycloakAccessible());
|
||||
assertTrue(status.isOverallHealthy());
|
||||
assertEquals("23.0.0", status.getKeycloakVersion());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCheckKeycloakHealthError() {
|
||||
when(syncService.isKeycloakAvailable()).thenThrow(new RuntimeException("Connection refused"));
|
||||
|
||||
HealthStatusDTO status = syncResource.checkKeycloakHealth();
|
||||
|
||||
assertFalse(status.isOverallHealthy());
|
||||
assertTrue(status.getErrorMessage().contains("Connection refused"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSyncUsers() {
|
||||
when(syncService.syncUsersFromRealm(REALM)).thenReturn(10);
|
||||
|
||||
SyncResultDTO result = syncResource.syncUsers(REALM);
|
||||
|
||||
assertTrue(result.isSuccess());
|
||||
assertEquals(10, result.getUsersCount());
|
||||
assertEquals(REALM, result.getRealmName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSyncUsersError() {
|
||||
when(syncService.syncUsersFromRealm(REALM)).thenThrow(new RuntimeException("Sync failed"));
|
||||
|
||||
SyncResultDTO result = syncResource.syncUsers(REALM);
|
||||
|
||||
assertFalse(result.isSuccess());
|
||||
assertEquals("Sync failed", result.getErrorMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSyncRoles() {
|
||||
when(syncService.syncRolesFromRealm(REALM)).thenReturn(5);
|
||||
|
||||
SyncResultDTO result = syncResource.syncRoles(REALM, null);
|
||||
|
||||
assertTrue(result.isSuccess());
|
||||
assertEquals(5, result.getRealmRolesCount());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package dev.lions.user.manager.resource;
|
||||
|
||||
import dev.lions.user.manager.client.KeycloakAdminClient;
|
||||
import dev.lions.user.manager.dto.common.UserSessionStatsDTO;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.keycloak.admin.client.resource.RealmResource;
|
||||
import org.keycloak.admin.client.resource.UserResource;
|
||||
import org.keycloak.admin.client.resource.UsersResource;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class UserMetricsResourceTest {
|
||||
|
||||
@Mock
|
||||
KeycloakAdminClient keycloakAdminClient;
|
||||
|
||||
@Mock
|
||||
RealmResource realmResource;
|
||||
|
||||
@Mock
|
||||
UsersResource usersResource;
|
||||
|
||||
@Mock
|
||||
UserResource userResource1;
|
||||
|
||||
@Mock
|
||||
UserResource userResource2;
|
||||
|
||||
@InjectMocks
|
||||
UserMetricsResource userMetricsResource;
|
||||
|
||||
@Test
|
||||
void testGetUserSessionStats() {
|
||||
// Préparer deux utilisateurs avec des sessions différentes
|
||||
UserRepresentation u1 = new UserRepresentation();
|
||||
u1.setId("u1");
|
||||
UserRepresentation u2 = new UserRepresentation();
|
||||
u2.setId("u2");
|
||||
|
||||
when(keycloakAdminClient.getRealm("test-realm")).thenReturn(realmResource);
|
||||
when(realmResource.users()).thenReturn(usersResource);
|
||||
when(usersResource.list()).thenReturn(List.of(u1, u2));
|
||||
|
||||
// u1 a 2 sessions, u2 en a 0
|
||||
when(usersResource.get("u1")).thenReturn(userResource1);
|
||||
when(usersResource.get("u2")).thenReturn(userResource2);
|
||||
when(userResource1.getUserSessions()).thenReturn(List.of(new org.keycloak.representations.idm.UserSessionRepresentation(),
|
||||
new org.keycloak.representations.idm.UserSessionRepresentation()));
|
||||
when(userResource2.getUserSessions()).thenReturn(List.of());
|
||||
|
||||
UserSessionStatsDTO stats = userMetricsResource.getUserSessionStats("test-realm");
|
||||
|
||||
assertNotNull(stats);
|
||||
assertEquals("test-realm", stats.getRealmName());
|
||||
assertEquals(2L, stats.getTotalUsers());
|
||||
assertEquals(2L, stats.getActiveSessions()); // 2 sessions au total
|
||||
assertEquals(1L, stats.getOnlineUsers()); // 1 utilisateur avec au moins une session
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetUserSessionStats_DefaultRealm() {
|
||||
when(keycloakAdminClient.getRealm("master")).thenReturn(realmResource);
|
||||
when(realmResource.users()).thenReturn(usersResource);
|
||||
when(usersResource.list()).thenReturn(List.of());
|
||||
|
||||
UserSessionStatsDTO stats = userMetricsResource.getUserSessionStats(null);
|
||||
|
||||
assertNotNull(stats);
|
||||
assertEquals("master", stats.getRealmName());
|
||||
assertEquals(0L, stats.getTotalUsers());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetUserSessionStats_OnError() {
|
||||
when(keycloakAdminClient.getRealm(anyString()))
|
||||
.thenThrow(new RuntimeException("KC error"));
|
||||
|
||||
Assertions.assertThrows(RuntimeException.class,
|
||||
() -> userMetricsResource.getUserSessionStats("realm"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
package dev.lions.user.manager.resource;
|
||||
|
||||
import dev.lions.user.manager.dto.user.*;
|
||||
import dev.lions.user.manager.service.UserService;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class UserResourceTest {
|
||||
|
||||
@Mock
|
||||
UserService userService;
|
||||
|
||||
@InjectMocks
|
||||
UserResource userResource;
|
||||
|
||||
private static final String REALM = "test-realm";
|
||||
|
||||
@Test
|
||||
void testSearchUsers() {
|
||||
UserSearchCriteriaDTO criteria = UserSearchCriteriaDTO.builder()
|
||||
.realmName(REALM)
|
||||
.searchTerm("test")
|
||||
.page(0)
|
||||
.pageSize(20)
|
||||
.build();
|
||||
|
||||
UserSearchResultDTO mockResult = UserSearchResultDTO.builder()
|
||||
.users(Collections.singletonList(UserDTO.builder().username("test").build()))
|
||||
.totalCount(1L)
|
||||
.build();
|
||||
|
||||
when(userService.searchUsers(any())).thenReturn(mockResult);
|
||||
|
||||
UserSearchResultDTO result = userResource.searchUsers(criteria);
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals(1, result.getTotalCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetUserById() {
|
||||
UserDTO user = UserDTO.builder().id("1").username("testuser").build();
|
||||
when(userService.getUserById("1", REALM)).thenReturn(Optional.of(user));
|
||||
|
||||
UserDTO result = userResource.getUserById("1", REALM);
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals(user, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetUserByIdNotFound() {
|
||||
when(userService.getUserById("1", REALM)).thenReturn(Optional.empty());
|
||||
|
||||
assertThrows(RuntimeException.class, () -> userResource.getUserById("1", REALM));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetAllUsers() {
|
||||
UserSearchResultDTO mockResult = UserSearchResultDTO.builder()
|
||||
.users(Collections.emptyList())
|
||||
.totalCount(0L)
|
||||
.build();
|
||||
when(userService.getAllUsers(REALM, 0, 20)).thenReturn(mockResult);
|
||||
|
||||
UserSearchResultDTO result = userResource.getAllUsers(REALM, 0, 20);
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals(0, result.getTotalCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreateUser() {
|
||||
UserDTO newUser = UserDTO.builder().username("newuser").email("new@test.com").build();
|
||||
UserDTO createdUser = UserDTO.builder().id("123").username("newuser").email("new@test.com").build();
|
||||
|
||||
when(userService.createUser(any(), eq(REALM))).thenReturn(createdUser);
|
||||
|
||||
Response response = userResource.createUser(newUser, REALM);
|
||||
|
||||
assertEquals(201, response.getStatus());
|
||||
assertEquals(createdUser, response.getEntity());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testUpdateUser() {
|
||||
UserDTO updateUser = UserDTO.builder()
|
||||
.username("updated")
|
||||
.prenom("John")
|
||||
.nom("Doe")
|
||||
.email("john.doe@test.com")
|
||||
.build();
|
||||
UserDTO updatedUser = UserDTO.builder()
|
||||
.id("1")
|
||||
.username("updated")
|
||||
.prenom("John")
|
||||
.nom("Doe")
|
||||
.email("john.doe@test.com")
|
||||
.build();
|
||||
|
||||
when(userService.updateUser(eq("1"), any(), eq(REALM))).thenReturn(updatedUser);
|
||||
|
||||
UserDTO result = userResource.updateUser("1", updateUser, REALM);
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals(updatedUser, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDeleteUser() {
|
||||
doNothing().when(userService).deleteUser("1", REALM, false);
|
||||
|
||||
userResource.deleteUser("1", REALM, false);
|
||||
|
||||
verify(userService).deleteUser("1", REALM, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testActivateUser() {
|
||||
doNothing().when(userService).activateUser("1", REALM);
|
||||
|
||||
userResource.activateUser("1", REALM);
|
||||
|
||||
verify(userService).activateUser("1", REALM);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDeactivateUser() {
|
||||
doNothing().when(userService).deactivateUser("1", REALM, "reason");
|
||||
|
||||
userResource.deactivateUser("1", REALM, "reason");
|
||||
|
||||
verify(userService).deactivateUser("1", REALM, "reason");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testResetPassword() {
|
||||
doNothing().when(userService).resetPassword("1", REALM, "newpassword", true);
|
||||
|
||||
PasswordResetRequestDTO request = PasswordResetRequestDTO.builder()
|
||||
.password("newpassword")
|
||||
.temporary(true)
|
||||
.build();
|
||||
|
||||
userResource.resetPassword("1", REALM, request);
|
||||
|
||||
verify(userService).resetPassword("1", REALM, "newpassword", true);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSendVerificationEmail() {
|
||||
doNothing().when(userService).sendVerificationEmail("1", REALM);
|
||||
|
||||
userResource.sendVerificationEmail("1", REALM);
|
||||
|
||||
verify(userService).sendVerificationEmail("1", REALM);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLogoutAllSessions() {
|
||||
when(userService.logoutAllSessions("1", REALM)).thenReturn(5);
|
||||
|
||||
SessionsRevokedDTO result = userResource.logoutAllSessions("1", REALM);
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals(5, result.getCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetActiveSessions() {
|
||||
when(userService.getActiveSessions("1", REALM)).thenReturn(Collections.singletonList("session-1"));
|
||||
|
||||
List<String> result = userResource.getActiveSessions("1", REALM);
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals(1, result.size());
|
||||
assertEquals("session-1", result.get(0));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package dev.lions.user.manager.security;
|
||||
|
||||
import jakarta.ws.rs.container.ContainerRequestContext;
|
||||
import jakarta.ws.rs.core.SecurityContext;
|
||||
import jakarta.ws.rs.core.UriInfo;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
* Tests unitaires pour DevSecurityContextProducer
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class DevSecurityContextProducerTest {
|
||||
|
||||
@Mock
|
||||
private ContainerRequestContext requestContext;
|
||||
|
||||
@Mock
|
||||
private UriInfo uriInfo;
|
||||
|
||||
@Mock
|
||||
private SecurityContext originalSecurityContext;
|
||||
|
||||
private DevSecurityContextProducer producer;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws Exception {
|
||||
producer = new DevSecurityContextProducer();
|
||||
|
||||
// Injecter les propriétés via reflection
|
||||
setField("profile", "dev");
|
||||
setField("oidcEnabled", false);
|
||||
}
|
||||
|
||||
private void setField(String fieldName, Object value) throws Exception {
|
||||
Field field = DevSecurityContextProducer.class.getDeclaredField(fieldName);
|
||||
field.setAccessible(true);
|
||||
field.set(producer, value);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFilter_DevMode() throws Exception {
|
||||
setField("profile", "dev");
|
||||
setField("oidcEnabled", true);
|
||||
|
||||
when(requestContext.getUriInfo()).thenReturn(uriInfo);
|
||||
when(uriInfo.getPath()).thenReturn("/api/users");
|
||||
when(requestContext.getSecurityContext()).thenReturn(originalSecurityContext);
|
||||
|
||||
producer.filter(requestContext);
|
||||
|
||||
verify(requestContext, times(1)).setSecurityContext(any(SecurityContext.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFilter_ProdMode() throws Exception {
|
||||
setField("profile", "prod");
|
||||
setField("oidcEnabled", true);
|
||||
|
||||
// En mode prod, on n'a pas besoin de mocker getUriInfo car le code ne l'utilise pas
|
||||
producer.filter(requestContext);
|
||||
|
||||
verify(requestContext, never()).setSecurityContext(any(SecurityContext.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFilter_OidcDisabled() throws Exception {
|
||||
setField("profile", "prod");
|
||||
setField("oidcEnabled", false);
|
||||
|
||||
when(requestContext.getUriInfo()).thenReturn(uriInfo);
|
||||
when(uriInfo.getPath()).thenReturn("/api/users");
|
||||
when(requestContext.getSecurityContext()).thenReturn(originalSecurityContext);
|
||||
|
||||
producer.filter(requestContext);
|
||||
|
||||
verify(requestContext, times(1)).setSecurityContext(any(SecurityContext.class));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
package dev.lions.user.manager.service.impl;
|
||||
|
||||
import dev.lions.user.manager.dto.audit.AuditLogDTO;
|
||||
import dev.lions.user.manager.enums.audit.TypeActionAudit;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Tests supplémentaires pour AuditServiceImpl pour améliorer la couverture
|
||||
*/
|
||||
class AuditServiceImplAdditionalTest {
|
||||
|
||||
private AuditServiceImpl auditService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
auditService = new AuditServiceImpl();
|
||||
auditService.auditEnabled = true;
|
||||
auditService.logToDatabase = false;
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFindByActeur_WithDates() {
|
||||
auditService.logSuccess(TypeActionAudit.USER_CREATE, "USER", "1", "user1", "realm1", "admin", "Created");
|
||||
auditService.logSuccess(TypeActionAudit.USER_UPDATE, "USER", "2", "user2", "realm1", "admin", "Updated");
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
LocalDateTime past = now.minusDays(1);
|
||||
LocalDateTime future = now.plusDays(1);
|
||||
|
||||
List<AuditLogDTO> logs = auditService.findByActeur("admin", past, future, 0, 10);
|
||||
|
||||
assertNotNull(logs);
|
||||
assertTrue(logs.size() > 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFindByRealm_WithDates() {
|
||||
auditService.logSuccess(TypeActionAudit.USER_CREATE, "USER", "1", "user1", "realm1", "admin", "Created");
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
LocalDateTime past = now.minusDays(1);
|
||||
LocalDateTime future = now.plusDays(1);
|
||||
|
||||
List<AuditLogDTO> logs = auditService.findByRealm("realm1", past, future, 0, 10);
|
||||
|
||||
assertNotNull(logs);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFindByRessource() {
|
||||
auditService.logSuccess(TypeActionAudit.USER_CREATE, "USER", "1", "user1", "realm1", "admin", "Created");
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
LocalDateTime past = now.minusDays(1);
|
||||
LocalDateTime future = now.plusDays(1);
|
||||
|
||||
List<AuditLogDTO> logs = auditService.findByRessource("USER", "1", past, future, 0, 10);
|
||||
|
||||
assertNotNull(logs);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCountByActionType() {
|
||||
auditService.logSuccess(TypeActionAudit.USER_CREATE, "USER", "1", "user1", "realm1", "admin", "Created");
|
||||
auditService.logSuccess(TypeActionAudit.USER_CREATE, "USER", "2", "user2", "realm1", "admin", "Created");
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
LocalDateTime past = now.minusDays(1);
|
||||
LocalDateTime future = now.plusDays(1);
|
||||
|
||||
java.util.Map<TypeActionAudit, Long> counts = auditService.countByActionType("realm1", past, future);
|
||||
|
||||
assertNotNull(counts);
|
||||
assertTrue(counts.containsKey(TypeActionAudit.USER_CREATE));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCountByActeur() {
|
||||
auditService.logSuccess(TypeActionAudit.USER_CREATE, "USER", "1", "user1", "realm1", "admin", "Created");
|
||||
auditService.logSuccess(TypeActionAudit.USER_UPDATE, "USER", "2", "user2", "realm1", "admin", "Updated");
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
LocalDateTime past = now.minusDays(1);
|
||||
LocalDateTime future = now.plusDays(1);
|
||||
|
||||
java.util.Map<String, Long> counts = auditService.countByActeur("realm1", past, future);
|
||||
|
||||
assertNotNull(counts);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCountSuccessVsFailure() {
|
||||
auditService.logSuccess(TypeActionAudit.USER_CREATE, "USER", "1", "user1", "realm1", "admin", "Created");
|
||||
auditService.logFailure(TypeActionAudit.USER_CREATE, "USER", "2", "user2", "realm1", "admin", "Failed", "Error");
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
LocalDateTime past = now.minusDays(1);
|
||||
LocalDateTime future = now.plusDays(1);
|
||||
|
||||
java.util.Map<String, Long> result = auditService.countSuccessVsFailure("realm1", past, future);
|
||||
|
||||
assertNotNull(result);
|
||||
assertTrue(result.containsKey("success"));
|
||||
assertTrue(result.containsKey("failure"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testExportToCSV() {
|
||||
auditService.logSuccess(TypeActionAudit.USER_CREATE, "USER", "1", "user1", "realm1", "admin", "Created");
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
LocalDateTime past = now.minusDays(1);
|
||||
LocalDateTime future = now.plusDays(1);
|
||||
|
||||
String csv = auditService.exportToCSV("realm1", past, future);
|
||||
|
||||
assertNotNull(csv);
|
||||
assertTrue(csv.length() > 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPurgeOldLogs() {
|
||||
// Créer des logs anciens
|
||||
for (int i = 0; i < 10; i++) {
|
||||
auditService.logSuccess(TypeActionAudit.USER_CREATE, "USER", String.valueOf(i),
|
||||
"user" + i, "realm1", "admin", "Created");
|
||||
}
|
||||
|
||||
LocalDateTime cutoffDate = LocalDateTime.now().minusDays(30);
|
||||
long purged = auditService.purgeOldLogs(cutoffDate);
|
||||
|
||||
assertTrue(purged >= 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetTotalCount() {
|
||||
auditService.logSuccess(TypeActionAudit.USER_CREATE, "USER", "1", "user1", "realm1", "admin", "Created");
|
||||
auditService.logSuccess(TypeActionAudit.USER_UPDATE, "USER", "2", "user2", "realm1", "admin", "Updated");
|
||||
|
||||
long total = auditService.getTotalCount();
|
||||
|
||||
assertEquals(2, total);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,322 @@
|
||||
package dev.lions.user.manager.service.impl;
|
||||
|
||||
import dev.lions.user.manager.dto.audit.AuditLogDTO;
|
||||
import dev.lions.user.manager.enums.audit.TypeActionAudit;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Tests complets pour AuditServiceImpl pour atteindre 100% de couverture
|
||||
* Couvre les branches manquantes : auditEnabled=false, acteurUsername="*", dates null, etc.
|
||||
*/
|
||||
class AuditServiceImplCompleteTest {
|
||||
|
||||
private AuditServiceImpl auditService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
auditService = new AuditServiceImpl();
|
||||
auditService.auditEnabled = true;
|
||||
auditService.logToDatabase = false;
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLogAction_AuditDisabled() {
|
||||
auditService.auditEnabled = false;
|
||||
|
||||
AuditLogDTO auditLog = AuditLogDTO.builder()
|
||||
.typeAction(TypeActionAudit.USER_CREATE)
|
||||
.acteurUsername("admin")
|
||||
.build();
|
||||
|
||||
AuditLogDTO result = auditService.logAction(auditLog);
|
||||
|
||||
assertEquals(auditLog, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLogAction_WithId() {
|
||||
AuditLogDTO auditLog = AuditLogDTO.builder()
|
||||
.id("custom-id")
|
||||
.typeAction(TypeActionAudit.USER_CREATE)
|
||||
.acteurUsername("admin")
|
||||
.build();
|
||||
|
||||
AuditLogDTO result = auditService.logAction(auditLog);
|
||||
|
||||
assertEquals("custom-id", result.getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLogAction_WithDateAction() {
|
||||
LocalDateTime customDate = LocalDateTime.now().minusDays(1);
|
||||
AuditLogDTO auditLog = AuditLogDTO.builder()
|
||||
.typeAction(TypeActionAudit.USER_CREATE)
|
||||
.acteurUsername("admin")
|
||||
.dateAction(customDate)
|
||||
.build();
|
||||
|
||||
AuditLogDTO result = auditService.logAction(auditLog);
|
||||
|
||||
assertEquals(customDate, result.getDateAction());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSearchLogs_WithWildcardActeur() {
|
||||
auditService.logSuccess(TypeActionAudit.USER_CREATE, "USER", "1", "user1", "realm1", "admin", "Created");
|
||||
auditService.logSuccess(TypeActionAudit.USER_UPDATE, "USER", "2", "user2", "realm1", "user2", "Updated");
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
LocalDateTime past = now.minusDays(1);
|
||||
LocalDateTime future = now.plusDays(1);
|
||||
|
||||
// Test avec acteurUsername = "*" (wildcard)
|
||||
List<AuditLogDTO> logs = auditService.findByActeur("*", past, future, 0, 10);
|
||||
|
||||
assertNotNull(logs);
|
||||
assertTrue(logs.size() >= 2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSearchLogs_WithNullDates() {
|
||||
auditService.logSuccess(TypeActionAudit.USER_CREATE, "USER", "1", "user1", "realm1", "admin", "Created");
|
||||
|
||||
// Test avec dates null
|
||||
List<AuditLogDTO> logs = auditService.findByActeur("admin", null, null, 0, 10);
|
||||
|
||||
assertNotNull(logs);
|
||||
assertTrue(logs.size() > 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSearchLogs_WithNullTypeAction() {
|
||||
auditService.logSuccess(TypeActionAudit.USER_CREATE, "USER", "1", "user1", "realm1", "admin", "Created");
|
||||
auditService.logSuccess(TypeActionAudit.USER_UPDATE, "USER", "2", "user2", "realm1", "admin", "Updated");
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
LocalDateTime past = now.minusDays(1);
|
||||
LocalDateTime future = now.plusDays(1);
|
||||
|
||||
// Test avec typeAction null (via findByRealm qui ne filtre pas par typeAction)
|
||||
List<AuditLogDTO> logs = auditService.findByRealm("realm1", past, future, 0, 10);
|
||||
|
||||
assertNotNull(logs);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSearchLogs_WithNullRessourceType() {
|
||||
auditService.logSuccess(TypeActionAudit.USER_CREATE, "USER", "1", "user1", "realm1", "admin", "Created");
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
LocalDateTime past = now.minusDays(1);
|
||||
LocalDateTime future = now.plusDays(1);
|
||||
|
||||
// Test avec ressourceType null (via findByRealm)
|
||||
List<AuditLogDTO> logs = auditService.findByRealm("realm1", past, future, 0, 10);
|
||||
|
||||
assertNotNull(logs);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFindFailures() {
|
||||
auditService.logSuccess(TypeActionAudit.USER_CREATE, "USER", "1", "user1", "realm1", "admin", "Created");
|
||||
auditService.logFailure(TypeActionAudit.USER_CREATE, "USER", "2", "user2", "realm1", "admin", "Failed", "Error");
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
LocalDateTime past = now.minusDays(1);
|
||||
LocalDateTime future = now.plusDays(1);
|
||||
|
||||
List<AuditLogDTO> failures = auditService.findFailures("realm1", past, future, 0, 10);
|
||||
|
||||
assertNotNull(failures);
|
||||
assertTrue(failures.size() > 0);
|
||||
assertFalse(failures.get(0).isSuccessful());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFindCriticalActions_UserDelete() {
|
||||
auditService.logSuccess(TypeActionAudit.USER_DELETE, "USER", "1", "user1", "realm1", "admin", "Deleted");
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
LocalDateTime past = now.minusDays(1);
|
||||
LocalDateTime future = now.plusDays(1);
|
||||
|
||||
List<AuditLogDTO> critical = auditService.findCriticalActions("realm1", past, future, 0, 10);
|
||||
|
||||
assertNotNull(critical);
|
||||
assertTrue(critical.size() > 0);
|
||||
assertEquals(TypeActionAudit.USER_DELETE, critical.get(0).getTypeAction());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFindCriticalActions_RoleDelete() {
|
||||
auditService.logSuccess(TypeActionAudit.ROLE_DELETE, "ROLE", "1", "role1", "realm1", "admin", "Deleted");
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
LocalDateTime past = now.minusDays(1);
|
||||
LocalDateTime future = now.plusDays(1);
|
||||
|
||||
List<AuditLogDTO> critical = auditService.findCriticalActions("realm1", past, future, 0, 10);
|
||||
|
||||
assertNotNull(critical);
|
||||
assertTrue(critical.size() > 0);
|
||||
assertEquals(TypeActionAudit.ROLE_DELETE, critical.get(0).getTypeAction());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFindCriticalActions_SessionRevokeAll() {
|
||||
auditService.logSuccess(TypeActionAudit.SESSION_REVOKE_ALL, "SESSION", "1", "session1", "realm1", "admin", "Revoked");
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
LocalDateTime past = now.minusDays(1);
|
||||
LocalDateTime future = now.plusDays(1);
|
||||
|
||||
List<AuditLogDTO> critical = auditService.findCriticalActions("realm1", past, future, 0, 10);
|
||||
|
||||
assertNotNull(critical);
|
||||
assertTrue(critical.size() > 0);
|
||||
assertEquals(TypeActionAudit.SESSION_REVOKE_ALL, critical.get(0).getTypeAction());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFindCriticalActions_WithDateFilters() {
|
||||
LocalDateTime oldDate = LocalDateTime.now().minusDays(10);
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
LocalDateTime past = now.minusDays(1);
|
||||
LocalDateTime future = now.plusDays(1);
|
||||
|
||||
// Créer un log ancien (hors de la plage)
|
||||
AuditLogDTO oldLog = AuditLogDTO.builder()
|
||||
.typeAction(TypeActionAudit.USER_DELETE)
|
||||
.acteurUsername("admin")
|
||||
.dateAction(oldDate)
|
||||
.build();
|
||||
auditService.logAction(oldLog);
|
||||
|
||||
// Créer un log récent (dans la plage)
|
||||
auditService.logSuccess(TypeActionAudit.USER_DELETE, "USER", "2", "user2", "realm1", "admin", "Deleted");
|
||||
|
||||
List<AuditLogDTO> critical = auditService.findCriticalActions("realm1", past, future, 0, 10);
|
||||
|
||||
assertNotNull(critical);
|
||||
// Seul le log récent devrait être retourné
|
||||
assertTrue(critical.size() >= 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetAuditStatistics() {
|
||||
auditService.logSuccess(TypeActionAudit.USER_CREATE, "USER", "1", "user1", "realm1", "admin", "Created");
|
||||
auditService.logFailure(TypeActionAudit.USER_CREATE, "USER", "2", "user2", "realm1", "admin", "Failed", "Error");
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
LocalDateTime past = now.minusDays(1);
|
||||
LocalDateTime future = now.plusDays(1);
|
||||
|
||||
Map<String, Object> stats = auditService.getAuditStatistics("realm1", past, future);
|
||||
|
||||
assertNotNull(stats);
|
||||
assertTrue(stats.containsKey("total"));
|
||||
assertTrue(stats.containsKey("success"));
|
||||
assertTrue(stats.containsKey("failure"));
|
||||
assertTrue(stats.containsKey("byActionType"));
|
||||
assertTrue(stats.containsKey("byActeur"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testExportToCSV_WithNullValues() {
|
||||
AuditLogDTO auditLog = AuditLogDTO.builder()
|
||||
.typeAction(TypeActionAudit.USER_CREATE)
|
||||
.acteurUsername("admin")
|
||||
.ressourceType("USER")
|
||||
.ressourceId("1")
|
||||
.success(true)
|
||||
.ipAddress(null)
|
||||
.description(null)
|
||||
.errorMessage(null)
|
||||
.build();
|
||||
auditService.logAction(auditLog);
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
LocalDateTime past = now.minusDays(1);
|
||||
LocalDateTime future = now.plusDays(1);
|
||||
|
||||
String csv = auditService.exportToCSV("realm1", past, future);
|
||||
|
||||
assertNotNull(csv);
|
||||
assertTrue(csv.contains("admin"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testExportToCSV_WithQuotesInDescription() {
|
||||
AuditLogDTO auditLog = AuditLogDTO.builder()
|
||||
.typeAction(TypeActionAudit.USER_CREATE)
|
||||
.acteurUsername("admin")
|
||||
.ressourceType("USER")
|
||||
.ressourceId("1")
|
||||
.success(true)
|
||||
.description("Test \"quoted\" description")
|
||||
.errorMessage("Error \"message\"")
|
||||
.build();
|
||||
auditService.logAction(auditLog);
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
LocalDateTime past = now.minusDays(1);
|
||||
LocalDateTime future = now.plusDays(1);
|
||||
|
||||
String csv = auditService.exportToCSV("realm1", past, future);
|
||||
|
||||
assertNotNull(csv);
|
||||
// Les guillemets devraient être échappés
|
||||
assertTrue(csv.contains("\"\""));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testClearAll() {
|
||||
auditService.logSuccess(TypeActionAudit.USER_CREATE, "USER", "1", "user1", "realm1", "admin", "Created");
|
||||
|
||||
assertEquals(1, auditService.getTotalCount());
|
||||
|
||||
auditService.clearAll();
|
||||
|
||||
assertEquals(0, auditService.getTotalCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFindByTypeAction() {
|
||||
auditService.logSuccess(TypeActionAudit.USER_CREATE, "USER", "1", "user1", "realm1", "admin", "Created");
|
||||
auditService.logSuccess(TypeActionAudit.USER_UPDATE, "USER", "2", "user2", "realm1", "admin", "Updated");
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
LocalDateTime past = now.minusDays(1);
|
||||
LocalDateTime future = now.plusDays(1);
|
||||
|
||||
List<AuditLogDTO> logs = auditService.findByTypeAction(TypeActionAudit.USER_CREATE, "realm1", past, future, 0, 10);
|
||||
|
||||
assertNotNull(logs);
|
||||
assertTrue(logs.size() > 0);
|
||||
assertEquals(TypeActionAudit.USER_CREATE, logs.get(0).getTypeAction());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSearchLogs_WithNullSuccess() {
|
||||
auditService.logSuccess(TypeActionAudit.USER_CREATE, "USER", "1", "user1", "realm1", "admin", "Created");
|
||||
auditService.logFailure(TypeActionAudit.USER_CREATE, "USER", "2", "user2", "realm1", "admin", "Failed", "Error");
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
LocalDateTime past = now.minusDays(1);
|
||||
LocalDateTime future = now.plusDays(1);
|
||||
|
||||
// findByRealm ne filtre pas par success, donc success = null
|
||||
List<AuditLogDTO> logs = auditService.findByRealm("realm1", past, future, 0, 10);
|
||||
|
||||
assertNotNull(logs);
|
||||
assertTrue(logs.size() >= 2);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
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.server.impl.entity.AuditLogEntity;
|
||||
import dev.lions.user.manager.server.impl.mapper.AuditLogMapper;
|
||||
import dev.lions.user.manager.server.impl.repository.AuditLogRepository;
|
||||
import io.quarkus.hibernate.orm.panache.PanacheQuery;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.mockito.junit.jupiter.MockitoSettings;
|
||||
import org.mockito.quality.Strictness;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@MockitoSettings(strictness = Strictness.LENIENT)
|
||||
class AuditServiceImplTest {
|
||||
|
||||
@Mock
|
||||
AuditLogRepository auditLogRepository;
|
||||
|
||||
@Mock
|
||||
AuditLogMapper auditLogMapper;
|
||||
|
||||
@InjectMocks
|
||||
AuditServiceImpl auditService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
auditService.auditEnabled = true;
|
||||
auditService.logToDatabase = true;
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLogAction() {
|
||||
AuditLogDTO log = new AuditLogDTO();
|
||||
log.setTypeAction(TypeActionAudit.USER_CREATE);
|
||||
log.setActeurUsername("admin");
|
||||
|
||||
when(auditLogMapper.toEntity(any(AuditLogDTO.class))).thenReturn(new AuditLogEntity());
|
||||
|
||||
auditService.logAction(log);
|
||||
|
||||
verify(auditLogRepository).persist(any(AuditLogEntity.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLogDisabled() {
|
||||
auditService.auditEnabled = false;
|
||||
AuditLogDTO log = new AuditLogDTO();
|
||||
|
||||
auditService.logAction(log);
|
||||
|
||||
verify(auditLogRepository, never()).persist(any(AuditLogEntity.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLogSuccess() {
|
||||
when(auditLogMapper.toEntity(any(AuditLogDTO.class))).thenReturn(new AuditLogEntity());
|
||||
|
||||
auditService.logSuccess(TypeActionAudit.USER_CREATE, "USER", "1", "user", "realm", "admin", "desc");
|
||||
|
||||
verify(auditLogRepository).persist(any(AuditLogEntity.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLogFailure() {
|
||||
when(auditLogMapper.toEntity(any(AuditLogDTO.class))).thenReturn(new AuditLogEntity());
|
||||
|
||||
auditService.logFailure(TypeActionAudit.USER_CREATE, "USER", "1", "user", "realm", "admin", "ERR", "Error");
|
||||
|
||||
verify(auditLogRepository).persist(any(AuditLogEntity.class));
|
||||
|
||||
// Test findFailures mock logic
|
||||
when(auditLogRepository.search(anyString(), any(), any(), any(), any(), eq(false), anyInt(), anyInt()))
|
||||
.thenReturn(Collections.singletonList(new AuditLogEntity()));
|
||||
when(auditLogMapper.toDTOList(anyList())).thenReturn(Collections.singletonList(new AuditLogDTO()));
|
||||
|
||||
List<AuditLogDTO> failures = auditService.findFailures("realm", null, null, 0, 10);
|
||||
assertEquals(1, failures.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSearchLogs() {
|
||||
// Mocking repo results
|
||||
when(auditLogRepository.search(any(), anyString(), any(), any(), any(), any(), anyInt(), anyInt()))
|
||||
.thenReturn(Collections.singletonList(new AuditLogEntity()));
|
||||
when(auditLogMapper.toDTOList(anyList())).thenReturn(Collections.singletonList(new AuditLogDTO()));
|
||||
|
||||
List<AuditLogDTO> byActeur = auditService.findByActeur("admin1", null, null, 0, 10);
|
||||
assertNotNull(byActeur);
|
||||
assertFalse(byActeur.isEmpty());
|
||||
|
||||
when(auditLogRepository.search(anyString(), any(), any(), any(), anyString(), any(), anyInt(), anyInt()))
|
||||
.thenReturn(Collections.singletonList(new AuditLogEntity()));
|
||||
|
||||
List<AuditLogDTO> byType = auditService.findByTypeAction(TypeActionAudit.ROLE_CREATE, "realm", null, null, 0,
|
||||
10);
|
||||
assertNotNull(byType);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testClearAll() {
|
||||
auditService.clearAll();
|
||||
verify(auditLogRepository).deleteAll();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
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 org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
* Tests unitaires pour RealmAuthorizationServiceImpl
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class RealmAuthorizationServiceImplTest {
|
||||
|
||||
@Mock
|
||||
private AuditService auditService;
|
||||
|
||||
@InjectMocks
|
||||
private RealmAuthorizationServiceImpl realmAuthorizationService;
|
||||
|
||||
private RealmAssignmentDTO assignment;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
assignment = RealmAssignmentDTO.builder()
|
||||
.id("assignment-1")
|
||||
.userId("user-1")
|
||||
.username("testuser")
|
||||
.email("test@example.com")
|
||||
.realmName("realm1")
|
||||
.isSuperAdmin(false)
|
||||
.active(true)
|
||||
.assignedAt(LocalDateTime.now())
|
||||
.assignedBy("admin")
|
||||
.build();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetAllAssignments_Empty() {
|
||||
List<RealmAssignmentDTO> assignments = realmAuthorizationService.getAllAssignments();
|
||||
assertTrue(assignments.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetAllAssignments_WithAssignments() {
|
||||
realmAuthorizationService.assignRealmToUser(assignment);
|
||||
List<RealmAssignmentDTO> assignments = realmAuthorizationService.getAllAssignments();
|
||||
assertEquals(1, assignments.size());
|
||||
assertEquals("assignment-1", assignments.get(0).getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetAssignmentsByUser_Success() {
|
||||
realmAuthorizationService.assignRealmToUser(assignment);
|
||||
List<RealmAssignmentDTO> assignments = realmAuthorizationService.getAssignmentsByUser("user-1");
|
||||
assertEquals(1, assignments.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetAssignmentsByUser_Empty() {
|
||||
List<RealmAssignmentDTO> assignments = realmAuthorizationService.getAssignmentsByUser("user-1");
|
||||
assertTrue(assignments.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetAssignmentsByRealm_Success() {
|
||||
realmAuthorizationService.assignRealmToUser(assignment);
|
||||
List<RealmAssignmentDTO> assignments = realmAuthorizationService.getAssignmentsByRealm("realm1");
|
||||
assertEquals(1, assignments.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetAssignmentById_Success() {
|
||||
realmAuthorizationService.assignRealmToUser(assignment);
|
||||
Optional<RealmAssignmentDTO> found = realmAuthorizationService.getAssignmentById("assignment-1");
|
||||
assertTrue(found.isPresent());
|
||||
assertEquals("assignment-1", found.get().getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetAssignmentById_NotFound() {
|
||||
Optional<RealmAssignmentDTO> found = realmAuthorizationService.getAssignmentById("non-existent");
|
||||
assertFalse(found.isPresent());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCanManageRealm_SuperAdmin() {
|
||||
realmAuthorizationService.setSuperAdmin("user-1", true);
|
||||
assertTrue(realmAuthorizationService.canManageRealm("user-1", "any-realm"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCanManageRealm_WithAssignment() {
|
||||
realmAuthorizationService.assignRealmToUser(assignment);
|
||||
assertTrue(realmAuthorizationService.canManageRealm("user-1", "realm1"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCanManageRealm_NoAccess() {
|
||||
assertFalse(realmAuthorizationService.canManageRealm("user-1", "realm1"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testIsSuperAdmin_True() {
|
||||
realmAuthorizationService.setSuperAdmin("user-1", true);
|
||||
assertTrue(realmAuthorizationService.isSuperAdmin("user-1"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testIsSuperAdmin_False() {
|
||||
assertFalse(realmAuthorizationService.isSuperAdmin("user-1"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetAuthorizedRealms_SuperAdmin() {
|
||||
realmAuthorizationService.setSuperAdmin("user-1", true);
|
||||
List<String> realms = realmAuthorizationService.getAuthorizedRealms("user-1");
|
||||
assertTrue(realms.isEmpty()); // Super admin retourne liste vide
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetAuthorizedRealms_WithAssignments() {
|
||||
realmAuthorizationService.assignRealmToUser(assignment);
|
||||
List<String> realms = realmAuthorizationService.getAuthorizedRealms("user-1");
|
||||
assertEquals(1, realms.size());
|
||||
assertEquals("realm1", realms.get(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAssignRealmToUser_Success() {
|
||||
doNothing().when(auditService).logSuccess(
|
||||
any(TypeActionAudit.class),
|
||||
anyString(),
|
||||
anyString(),
|
||||
anyString(),
|
||||
anyString(),
|
||||
anyString(),
|
||||
anyString()
|
||||
);
|
||||
|
||||
RealmAssignmentDTO result = realmAuthorizationService.assignRealmToUser(assignment);
|
||||
assertNotNull(result);
|
||||
assertNotNull(result.getId());
|
||||
assertTrue(result.isActive());
|
||||
assertNotNull(result.getAssignedAt());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAssignRealmToUser_NoUserId() {
|
||||
assignment.setUserId(null);
|
||||
assertThrows(IllegalArgumentException.class, () -> {
|
||||
realmAuthorizationService.assignRealmToUser(assignment);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAssignRealmToUser_NoRealmName() {
|
||||
assignment.setRealmName(null);
|
||||
assertThrows(IllegalArgumentException.class, () -> {
|
||||
realmAuthorizationService.assignRealmToUser(assignment);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAssignRealmToUser_Duplicate() {
|
||||
doNothing().when(auditService).logSuccess(any(), anyString(), anyString(), anyString(), anyString(), anyString(), anyString());
|
||||
realmAuthorizationService.assignRealmToUser(assignment);
|
||||
assertThrows(IllegalArgumentException.class, () -> {
|
||||
realmAuthorizationService.assignRealmToUser(assignment);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRevokeRealmFromUser_Success() {
|
||||
doNothing().when(auditService).logSuccess(any(), anyString(), anyString(), anyString(), anyString(), anyString(), anyString());
|
||||
realmAuthorizationService.assignRealmToUser(assignment);
|
||||
realmAuthorizationService.revokeRealmFromUser("user-1", "realm1");
|
||||
assertFalse(realmAuthorizationService.canManageRealm("user-1", "realm1"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRevokeRealmFromUser_NotExists() {
|
||||
// Ne doit pas lever d'exception si l'assignation n'existe pas
|
||||
assertDoesNotThrow(() -> {
|
||||
realmAuthorizationService.revokeRealmFromUser("user-1", "realm1");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRevokeAllRealmsFromUser() {
|
||||
doNothing().when(auditService).logSuccess(any(), anyString(), anyString(), anyString(), anyString(), anyString(), anyString());
|
||||
realmAuthorizationService.assignRealmToUser(assignment);
|
||||
realmAuthorizationService.revokeAllRealmsFromUser("user-1");
|
||||
assertTrue(realmAuthorizationService.getAssignmentsByUser("user-1").isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSetSuperAdmin_True() {
|
||||
doNothing().when(auditService).logSuccess(any(), anyString(), anyString(), anyString(), anyString(), anyString(), anyString());
|
||||
realmAuthorizationService.setSuperAdmin("user-1", true);
|
||||
assertTrue(realmAuthorizationService.isSuperAdmin("user-1"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSetSuperAdmin_False() {
|
||||
doNothing().when(auditService).logSuccess(any(), anyString(), anyString(), anyString(), anyString(), anyString(), anyString());
|
||||
realmAuthorizationService.setSuperAdmin("user-1", true);
|
||||
realmAuthorizationService.setSuperAdmin("user-1", false);
|
||||
assertFalse(realmAuthorizationService.isSuperAdmin("user-1"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDeactivateAssignment_Success() {
|
||||
doNothing().when(auditService).logSuccess(any(), anyString(), anyString(), anyString(), anyString(), anyString(), anyString());
|
||||
realmAuthorizationService.assignRealmToUser(assignment);
|
||||
realmAuthorizationService.deactivateAssignment(assignment.getId());
|
||||
Optional<RealmAssignmentDTO> found = realmAuthorizationService.getAssignmentById(assignment.getId());
|
||||
assertTrue(found.isPresent());
|
||||
assertFalse(found.get().isActive());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDeactivateAssignment_NotFound() {
|
||||
assertThrows(IllegalArgumentException.class, () -> {
|
||||
realmAuthorizationService.deactivateAssignment("non-existent");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void testActivateAssignment_Success() {
|
||||
doNothing().when(auditService).logSuccess(any(), anyString(), anyString(), anyString(), anyString(), anyString(), anyString());
|
||||
realmAuthorizationService.assignRealmToUser(assignment);
|
||||
realmAuthorizationService.deactivateAssignment(assignment.getId());
|
||||
realmAuthorizationService.activateAssignment(assignment.getId());
|
||||
Optional<RealmAssignmentDTO> found = realmAuthorizationService.getAssignmentById(assignment.getId());
|
||||
assertTrue(found.isPresent());
|
||||
assertTrue(found.get().isActive());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCountAssignmentsByUser() {
|
||||
doNothing().when(auditService).logSuccess(any(), anyString(), anyString(), anyString(), anyString(), anyString(), anyString());
|
||||
realmAuthorizationService.assignRealmToUser(assignment);
|
||||
long count = realmAuthorizationService.countAssignmentsByUser("user-1");
|
||||
assertEquals(1, count);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCountUsersByRealm() {
|
||||
doNothing().when(auditService).logSuccess(any(), anyString(), anyString(), anyString(), anyString(), anyString(), anyString());
|
||||
realmAuthorizationService.assignRealmToUser(assignment);
|
||||
long count = realmAuthorizationService.countUsersByRealm("realm1");
|
||||
assertEquals(1, count);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAssignmentExists_True() {
|
||||
doNothing().when(auditService).logSuccess(any(), anyString(), anyString(), anyString(), anyString(), anyString(), anyString());
|
||||
realmAuthorizationService.assignRealmToUser(assignment);
|
||||
assertTrue(realmAuthorizationService.assignmentExists("user-1", "realm1"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAssignmentExists_False() {
|
||||
assertFalse(realmAuthorizationService.assignmentExists("user-1", "realm1"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,350 @@
|
||||
package dev.lions.user.manager.service.impl;
|
||||
|
||||
import dev.lions.user.manager.client.KeycloakAdminClient;
|
||||
import dev.lions.user.manager.dto.role.RoleDTO;
|
||||
import dev.lions.user.manager.enums.role.TypeRole;
|
||||
import dev.lions.user.manager.mapper.RoleMapper;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.keycloak.admin.client.Keycloak;
|
||||
import org.keycloak.admin.client.resource.*;
|
||||
import org.keycloak.representations.idm.ClientRepresentation;
|
||||
import org.keycloak.representations.idm.RoleRepresentation;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
* Tests complets pour RoleServiceImpl pour atteindre 100% de couverture
|
||||
* Couvre updateRole, deleteRole pour CLIENT_ROLE, createRealmRole avec rôle existant, etc.
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class RoleServiceImplCompleteTest {
|
||||
|
||||
@Mock
|
||||
private KeycloakAdminClient keycloakAdminClient;
|
||||
|
||||
@Mock
|
||||
private Keycloak keycloakInstance;
|
||||
|
||||
@Mock
|
||||
private RealmResource realmResource;
|
||||
|
||||
@Mock
|
||||
private RolesResource rolesResource;
|
||||
|
||||
@Mock
|
||||
private RoleResource roleResource;
|
||||
|
||||
@Mock
|
||||
private ClientsResource clientsResource;
|
||||
|
||||
@Mock
|
||||
private ClientResource clientResource;
|
||||
|
||||
@InjectMocks
|
||||
private RoleServiceImpl roleService;
|
||||
|
||||
private static final String REALM = "test-realm";
|
||||
private static final String ROLE_ID = "role-123";
|
||||
private static final String ROLE_NAME = "test-role";
|
||||
private static final String CLIENT_NAME = "test-client";
|
||||
private static final String INTERNAL_CLIENT_ID = "internal-client-id";
|
||||
|
||||
@Test
|
||||
void testCreateRealmRole_RoleAlreadyExists() {
|
||||
when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance);
|
||||
when(keycloakInstance.realm(REALM)).thenReturn(realmResource);
|
||||
when(realmResource.roles()).thenReturn(rolesResource);
|
||||
when(rolesResource.get(ROLE_NAME)).thenReturn(roleResource);
|
||||
|
||||
RoleRepresentation existingRole = new RoleRepresentation();
|
||||
existingRole.setName(ROLE_NAME);
|
||||
when(roleResource.toRepresentation()).thenReturn(existingRole);
|
||||
|
||||
RoleDTO roleDTO = RoleDTO.builder()
|
||||
.name(ROLE_NAME)
|
||||
.description("Test role")
|
||||
.build();
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () ->
|
||||
roleService.createRealmRole(roleDTO, REALM));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testUpdateRole_RealmRole_Success() {
|
||||
when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance);
|
||||
when(keycloakInstance.realm(REALM)).thenReturn(realmResource);
|
||||
when(realmResource.roles()).thenReturn(rolesResource);
|
||||
|
||||
// Mock getRealmRoleById
|
||||
RoleRepresentation roleRep = new RoleRepresentation();
|
||||
roleRep.setId(ROLE_ID);
|
||||
roleRep.setName(ROLE_NAME);
|
||||
when(rolesResource.list()).thenReturn(List.of(roleRep));
|
||||
when(rolesResource.get(ROLE_NAME)).thenReturn(roleResource);
|
||||
when(roleResource.toRepresentation()).thenReturn(roleRep);
|
||||
|
||||
RoleDTO roleDTO = RoleDTO.builder()
|
||||
.id(ROLE_ID)
|
||||
.name(ROLE_NAME)
|
||||
.description("Updated description")
|
||||
.build();
|
||||
|
||||
RoleDTO result = roleService.updateRole(ROLE_ID, roleDTO, REALM, TypeRole.REALM_ROLE, null);
|
||||
|
||||
assertNotNull(result);
|
||||
verify(roleResource).update(any(RoleRepresentation.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testUpdateRole_RealmRole_NotFound() {
|
||||
when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance);
|
||||
when(keycloakInstance.realm(REALM)).thenReturn(realmResource);
|
||||
when(realmResource.roles()).thenReturn(rolesResource);
|
||||
when(rolesResource.list()).thenReturn(Collections.emptyList());
|
||||
|
||||
RoleDTO roleDTO = RoleDTO.builder()
|
||||
.id(ROLE_ID)
|
||||
.name(ROLE_NAME)
|
||||
.build();
|
||||
|
||||
assertThrows(jakarta.ws.rs.NotFoundException.class, () ->
|
||||
roleService.updateRole(ROLE_ID, roleDTO, REALM, TypeRole.REALM_ROLE, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testUpdateRole_RealmRole_NoDescription() {
|
||||
when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance);
|
||||
when(keycloakInstance.realm(REALM)).thenReturn(realmResource);
|
||||
when(realmResource.roles()).thenReturn(rolesResource);
|
||||
|
||||
RoleRepresentation roleRep = new RoleRepresentation();
|
||||
roleRep.setId(ROLE_ID);
|
||||
roleRep.setName(ROLE_NAME);
|
||||
when(rolesResource.list()).thenReturn(List.of(roleRep));
|
||||
when(rolesResource.get(ROLE_NAME)).thenReturn(roleResource);
|
||||
when(roleResource.toRepresentation()).thenReturn(roleRep);
|
||||
|
||||
RoleDTO roleDTO = RoleDTO.builder()
|
||||
.id(ROLE_ID)
|
||||
.name(ROLE_NAME)
|
||||
.description(null) // No description
|
||||
.build();
|
||||
|
||||
RoleDTO result = roleService.updateRole(ROLE_ID, roleDTO, REALM, TypeRole.REALM_ROLE, null);
|
||||
|
||||
assertNotNull(result);
|
||||
verify(roleResource).update(any(RoleRepresentation.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testUpdateRole_ClientRole_Success() {
|
||||
when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance);
|
||||
when(keycloakInstance.realm(REALM)).thenReturn(realmResource);
|
||||
when(realmResource.clients()).thenReturn(clientsResource);
|
||||
|
||||
ClientRepresentation client = new ClientRepresentation();
|
||||
client.setId(INTERNAL_CLIENT_ID);
|
||||
when(clientsResource.findByClientId(CLIENT_NAME)).thenReturn(List.of(client));
|
||||
when(clientsResource.get(INTERNAL_CLIENT_ID)).thenReturn(clientResource);
|
||||
when(clientResource.roles()).thenReturn(rolesResource);
|
||||
|
||||
// Mock getRoleById
|
||||
RoleRepresentation roleRep = new RoleRepresentation();
|
||||
roleRep.setId(ROLE_ID);
|
||||
roleRep.setName(ROLE_NAME);
|
||||
when(rolesResource.list()).thenReturn(List.of(roleRep));
|
||||
when(rolesResource.get(ROLE_NAME)).thenReturn(roleResource);
|
||||
when(roleResource.toRepresentation()).thenReturn(roleRep);
|
||||
|
||||
RoleDTO roleDTO = RoleDTO.builder()
|
||||
.id(ROLE_ID)
|
||||
.name(ROLE_NAME)
|
||||
.description("Updated description")
|
||||
.build();
|
||||
|
||||
RoleDTO result = roleService.updateRole(ROLE_ID, roleDTO, REALM, TypeRole.CLIENT_ROLE, CLIENT_NAME);
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals(CLIENT_NAME, result.getClientId());
|
||||
verify(roleResource).update(any(RoleRepresentation.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testUpdateRole_ClientRole_ClientNotFound() {
|
||||
when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance);
|
||||
when(keycloakInstance.realm(REALM)).thenReturn(realmResource);
|
||||
when(realmResource.clients()).thenReturn(clientsResource);
|
||||
when(clientsResource.findByClientId(CLIENT_NAME)).thenReturn(Collections.emptyList());
|
||||
|
||||
RoleDTO roleDTO = RoleDTO.builder()
|
||||
.id(ROLE_ID)
|
||||
.name(ROLE_NAME)
|
||||
.build();
|
||||
|
||||
// getRoleById is called first, which will throw NotFoundException when client is not found
|
||||
// Actually, getRoleById returns Optional.empty() when client is not found
|
||||
// So it will throw NotFoundException for role not found
|
||||
assertThrows(jakarta.ws.rs.NotFoundException.class, () ->
|
||||
roleService.updateRole(ROLE_ID, roleDTO, REALM, TypeRole.CLIENT_ROLE, CLIENT_NAME));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testUpdateRole_ClientRole_NotFound() {
|
||||
when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance);
|
||||
when(keycloakInstance.realm(REALM)).thenReturn(realmResource);
|
||||
when(realmResource.clients()).thenReturn(clientsResource);
|
||||
|
||||
ClientRepresentation client = new ClientRepresentation();
|
||||
client.setId(INTERNAL_CLIENT_ID);
|
||||
when(clientsResource.findByClientId(CLIENT_NAME)).thenReturn(List.of(client));
|
||||
when(clientsResource.get(INTERNAL_CLIENT_ID)).thenReturn(clientResource);
|
||||
when(clientResource.roles()).thenReturn(rolesResource);
|
||||
when(rolesResource.list()).thenReturn(Collections.emptyList());
|
||||
|
||||
RoleDTO roleDTO = RoleDTO.builder()
|
||||
.id(ROLE_ID)
|
||||
.name(ROLE_NAME)
|
||||
.build();
|
||||
|
||||
assertThrows(jakarta.ws.rs.NotFoundException.class, () ->
|
||||
roleService.updateRole(ROLE_ID, roleDTO, REALM, TypeRole.CLIENT_ROLE, CLIENT_NAME));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testUpdateRole_UnsupportedType() {
|
||||
RoleDTO roleDTO = RoleDTO.builder()
|
||||
.id(ROLE_ID)
|
||||
.name(ROLE_NAME)
|
||||
.build();
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () ->
|
||||
roleService.updateRole(ROLE_ID, roleDTO, REALM, null, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDeleteRole_ClientRole_Success() {
|
||||
when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance);
|
||||
when(keycloakInstance.realm(REALM)).thenReturn(realmResource);
|
||||
when(realmResource.clients()).thenReturn(clientsResource);
|
||||
|
||||
ClientRepresentation client = new ClientRepresentation();
|
||||
client.setId(INTERNAL_CLIENT_ID);
|
||||
when(clientsResource.findByClientId(CLIENT_NAME)).thenReturn(List.of(client));
|
||||
when(clientsResource.get(INTERNAL_CLIENT_ID)).thenReturn(clientResource);
|
||||
when(clientResource.roles()).thenReturn(rolesResource);
|
||||
|
||||
// Mock getRoleById - getRoleById for CLIENT_ROLE only uses rolesResource.list()
|
||||
RoleRepresentation roleRep = new RoleRepresentation();
|
||||
roleRep.setId(ROLE_ID);
|
||||
roleRep.setName(ROLE_NAME);
|
||||
when(rolesResource.list()).thenReturn(List.of(roleRep));
|
||||
|
||||
roleService.deleteRole(ROLE_ID, REALM, TypeRole.CLIENT_ROLE, CLIENT_NAME);
|
||||
|
||||
verify(rolesResource).deleteRole(ROLE_NAME);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDeleteRole_ClientRole_ClientNotFound() {
|
||||
when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance);
|
||||
when(keycloakInstance.realm(REALM)).thenReturn(realmResource);
|
||||
when(realmResource.clients()).thenReturn(clientsResource);
|
||||
when(clientsResource.findByClientId(CLIENT_NAME)).thenReturn(Collections.emptyList());
|
||||
|
||||
// getRoleById is called first, which returns Optional.empty() when client is not found
|
||||
// So it will throw NotFoundException for role not found
|
||||
assertThrows(jakarta.ws.rs.NotFoundException.class, () ->
|
||||
roleService.deleteRole(ROLE_ID, REALM, TypeRole.CLIENT_ROLE, CLIENT_NAME));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDeleteRole_ClientRole_NotFound() {
|
||||
when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance);
|
||||
when(keycloakInstance.realm(REALM)).thenReturn(realmResource);
|
||||
when(realmResource.clients()).thenReturn(clientsResource);
|
||||
|
||||
ClientRepresentation client = new ClientRepresentation();
|
||||
client.setId(INTERNAL_CLIENT_ID);
|
||||
when(clientsResource.findByClientId(CLIENT_NAME)).thenReturn(List.of(client));
|
||||
when(clientsResource.get(INTERNAL_CLIENT_ID)).thenReturn(clientResource);
|
||||
when(clientResource.roles()).thenReturn(rolesResource);
|
||||
when(rolesResource.list()).thenReturn(Collections.emptyList());
|
||||
|
||||
assertThrows(jakarta.ws.rs.NotFoundException.class, () ->
|
||||
roleService.deleteRole(ROLE_ID, REALM, TypeRole.CLIENT_ROLE, CLIENT_NAME));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDeleteRole_UnsupportedType() {
|
||||
assertThrows(IllegalArgumentException.class, () ->
|
||||
roleService.deleteRole(ROLE_ID, REALM, null, null));
|
||||
}
|
||||
|
||||
// Note: getRealmRoleById is private, so we test it indirectly through updateRole
|
||||
// The exception path is tested via updateRole_RealmRole_NotFound
|
||||
|
||||
@Test
|
||||
void testGetAllRealmRoles_Success() {
|
||||
when(keycloakAdminClient.realmExists(REALM)).thenReturn(true);
|
||||
when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance);
|
||||
when(keycloakInstance.realm(REALM)).thenReturn(realmResource);
|
||||
when(realmResource.roles()).thenReturn(rolesResource);
|
||||
|
||||
RoleRepresentation role1 = new RoleRepresentation();
|
||||
role1.setName("role1");
|
||||
RoleRepresentation role2 = new RoleRepresentation();
|
||||
role2.setName("role2");
|
||||
when(rolesResource.list()).thenReturn(List.of(role1, role2));
|
||||
|
||||
var result = roleService.getAllRealmRoles(REALM);
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals(2, result.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetAllRealmRoles_With404InMessage() {
|
||||
when(keycloakAdminClient.realmExists(REALM)).thenReturn(true);
|
||||
when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance);
|
||||
when(keycloakInstance.realm(REALM)).thenReturn(realmResource);
|
||||
when(realmResource.roles()).thenReturn(rolesResource);
|
||||
when(rolesResource.list()).thenThrow(new RuntimeException("Server response is: 404"));
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () ->
|
||||
roleService.getAllRealmRoles(REALM));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetAllRealmRoles_WithNotInMessage() {
|
||||
when(keycloakAdminClient.realmExists(REALM)).thenReturn(true);
|
||||
when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance);
|
||||
when(keycloakInstance.realm(REALM)).thenReturn(realmResource);
|
||||
when(realmResource.roles()).thenReturn(rolesResource);
|
||||
when(rolesResource.list()).thenThrow(new RuntimeException("Not Found"));
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () ->
|
||||
roleService.getAllRealmRoles(REALM));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetAllRealmRoles_WithOtherException() {
|
||||
when(keycloakAdminClient.realmExists(REALM)).thenReturn(true);
|
||||
when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance);
|
||||
when(keycloakInstance.realm(REALM)).thenReturn(realmResource);
|
||||
when(realmResource.roles()).thenReturn(rolesResource);
|
||||
when(rolesResource.list()).thenThrow(new RuntimeException("Connection error"));
|
||||
|
||||
assertThrows(RuntimeException.class, () ->
|
||||
roleService.getAllRealmRoles(REALM));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,245 @@
|
||||
package dev.lions.user.manager.service.impl;
|
||||
|
||||
import dev.lions.user.manager.client.KeycloakAdminClient;
|
||||
import dev.lions.user.manager.dto.role.RoleDTO;
|
||||
import dev.lions.user.manager.enums.role.TypeRole;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.keycloak.admin.client.Keycloak;
|
||||
import org.keycloak.admin.client.resource.*;
|
||||
import org.keycloak.representations.idm.ClientRepresentation;
|
||||
import org.keycloak.representations.idm.RoleRepresentation;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
* Tests supplémentaires pour RoleServiceImpl pour améliorer la couverture
|
||||
* Couvre les méthodes : userHasRole, roleExists, countUsersWithRole
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class RoleServiceImplExtendedTest {
|
||||
|
||||
@Mock
|
||||
private KeycloakAdminClient keycloakAdminClient;
|
||||
|
||||
@Mock
|
||||
private Keycloak keycloakInstance;
|
||||
|
||||
@Mock
|
||||
private RealmResource realmResource;
|
||||
|
||||
@Mock
|
||||
private RolesResource rolesResource;
|
||||
|
||||
@Mock
|
||||
private RoleResource roleResource;
|
||||
|
||||
@Mock
|
||||
private UsersResource usersResource;
|
||||
|
||||
@Mock
|
||||
private UserResource userResource;
|
||||
|
||||
@Mock
|
||||
private RoleMappingResource roleMappingResource;
|
||||
|
||||
@Mock
|
||||
private RoleScopeResource realmLevelRoleScopeResource;
|
||||
|
||||
@Mock
|
||||
private RoleScopeResource clientLevelRoleScopeResource;
|
||||
|
||||
@Mock
|
||||
private ClientsResource clientsResource;
|
||||
|
||||
@InjectMocks
|
||||
private RoleServiceImpl roleService;
|
||||
|
||||
private static final String REALM = "test-realm";
|
||||
private static final String USER_ID = "user-123";
|
||||
private static final String ROLE_NAME = "admin";
|
||||
private static final String CLIENT_NAME = "test-client";
|
||||
|
||||
@Test
|
||||
void testUserHasRole_RealmRole_True() {
|
||||
when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance);
|
||||
when(keycloakInstance.realm(REALM)).thenReturn(realmResource);
|
||||
when(realmResource.users()).thenReturn(usersResource);
|
||||
when(usersResource.get(USER_ID)).thenReturn(userResource);
|
||||
when(userResource.roles()).thenReturn(roleMappingResource);
|
||||
when(roleMappingResource.realmLevel()).thenReturn(realmLevelRoleScopeResource);
|
||||
|
||||
RoleRepresentation role = new RoleRepresentation();
|
||||
role.setName(ROLE_NAME);
|
||||
when(realmLevelRoleScopeResource.listEffective()).thenReturn(List.of(role));
|
||||
|
||||
boolean result = roleService.userHasRole(USER_ID, ROLE_NAME, REALM, TypeRole.REALM_ROLE, null);
|
||||
|
||||
assertTrue(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testUserHasRole_RealmRole_False() {
|
||||
when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance);
|
||||
when(keycloakInstance.realm(REALM)).thenReturn(realmResource);
|
||||
when(realmResource.users()).thenReturn(usersResource);
|
||||
when(usersResource.get(USER_ID)).thenReturn(userResource);
|
||||
when(userResource.roles()).thenReturn(roleMappingResource);
|
||||
when(roleMappingResource.realmLevel()).thenReturn(realmLevelRoleScopeResource);
|
||||
when(realmLevelRoleScopeResource.listEffective()).thenReturn(Collections.emptyList());
|
||||
|
||||
boolean result = roleService.userHasRole(USER_ID, ROLE_NAME, REALM, TypeRole.REALM_ROLE, null);
|
||||
|
||||
assertFalse(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testUserHasRole_ClientRole_True() {
|
||||
when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance);
|
||||
when(keycloakInstance.realm(REALM)).thenReturn(realmResource);
|
||||
when(realmResource.clients()).thenReturn(clientsResource);
|
||||
when(realmResource.users()).thenReturn(usersResource);
|
||||
when(usersResource.get(USER_ID)).thenReturn(userResource);
|
||||
when(userResource.roles()).thenReturn(roleMappingResource);
|
||||
|
||||
ClientRepresentation client = new ClientRepresentation();
|
||||
client.setId("client-123");
|
||||
when(clientsResource.findByClientId(CLIENT_NAME)).thenReturn(List.of(client));
|
||||
when(roleMappingResource.clientLevel("client-123")).thenReturn(clientLevelRoleScopeResource);
|
||||
|
||||
RoleRepresentation role = new RoleRepresentation();
|
||||
role.setName(ROLE_NAME);
|
||||
when(clientLevelRoleScopeResource.listEffective()).thenReturn(List.of(role));
|
||||
|
||||
boolean result = roleService.userHasRole(USER_ID, ROLE_NAME, REALM, TypeRole.CLIENT_ROLE, CLIENT_NAME);
|
||||
|
||||
assertTrue(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testUserHasRole_ClientRole_ClientNotFound() {
|
||||
when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance);
|
||||
when(keycloakInstance.realm(REALM)).thenReturn(realmResource);
|
||||
when(realmResource.clients()).thenReturn(clientsResource);
|
||||
when(clientsResource.findByClientId(CLIENT_NAME)).thenReturn(Collections.emptyList());
|
||||
|
||||
boolean result = roleService.userHasRole(USER_ID, ROLE_NAME, REALM, TypeRole.CLIENT_ROLE, CLIENT_NAME);
|
||||
|
||||
assertFalse(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testUserHasRole_ClientRole_NullClientName() {
|
||||
boolean result = roleService.userHasRole(USER_ID, ROLE_NAME, REALM, TypeRole.CLIENT_ROLE, null);
|
||||
|
||||
assertFalse(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRoleExists_RealmRole_True() {
|
||||
when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance);
|
||||
when(keycloakInstance.realm(REALM)).thenReturn(realmResource);
|
||||
when(realmResource.roles()).thenReturn(rolesResource);
|
||||
when(rolesResource.get(ROLE_NAME)).thenReturn(roleResource);
|
||||
|
||||
RoleRepresentation role = new RoleRepresentation();
|
||||
role.setName(ROLE_NAME);
|
||||
when(roleResource.toRepresentation()).thenReturn(role);
|
||||
|
||||
boolean result = roleService.roleExists(ROLE_NAME, REALM, TypeRole.REALM_ROLE, null);
|
||||
|
||||
assertTrue(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRoleExists_RealmRole_False() {
|
||||
when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance);
|
||||
when(keycloakInstance.realm(REALM)).thenReturn(realmResource);
|
||||
when(realmResource.roles()).thenReturn(rolesResource);
|
||||
when(rolesResource.get(ROLE_NAME)).thenReturn(roleResource);
|
||||
when(roleResource.toRepresentation()).thenThrow(new jakarta.ws.rs.NotFoundException());
|
||||
|
||||
boolean result = roleService.roleExists(ROLE_NAME, REALM, TypeRole.REALM_ROLE, null);
|
||||
|
||||
assertFalse(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCountUsersWithRole_Success() {
|
||||
when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance);
|
||||
when(keycloakInstance.realm(REALM)).thenReturn(realmResource);
|
||||
when(realmResource.roles()).thenReturn(rolesResource);
|
||||
when(realmResource.users()).thenReturn(usersResource);
|
||||
|
||||
// Mock getRoleById
|
||||
RoleRepresentation roleRep = new RoleRepresentation();
|
||||
roleRep.setId("role-123");
|
||||
roleRep.setName(ROLE_NAME);
|
||||
when(rolesResource.list()).thenReturn(List.of(roleRep));
|
||||
|
||||
// Mock user list
|
||||
UserRepresentation user1 = new UserRepresentation();
|
||||
user1.setId("user-1");
|
||||
UserRepresentation user2 = new UserRepresentation();
|
||||
user2.setId("user-2");
|
||||
when(usersResource.list()).thenReturn(List.of(user1, user2));
|
||||
|
||||
// Mock userHasRole for each user
|
||||
when(usersResource.get("user-1")).thenReturn(userResource);
|
||||
when(usersResource.get("user-2")).thenReturn(userResource);
|
||||
when(userResource.roles()).thenReturn(roleMappingResource);
|
||||
when(roleMappingResource.realmLevel()).thenReturn(realmLevelRoleScopeResource);
|
||||
|
||||
RoleRepresentation role = new RoleRepresentation();
|
||||
role.setName(ROLE_NAME);
|
||||
// User 1 has role, user 2 doesn't
|
||||
when(realmLevelRoleScopeResource.listEffective())
|
||||
.thenReturn(List.of(role)) // user-1
|
||||
.thenReturn(Collections.emptyList()); // user-2
|
||||
|
||||
long count = roleService.countUsersWithRole("role-123", REALM, TypeRole.REALM_ROLE, null);
|
||||
|
||||
assertEquals(1, count);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCountUsersWithRole_RoleNotFound() {
|
||||
when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance);
|
||||
when(keycloakInstance.realm(REALM)).thenReturn(realmResource);
|
||||
when(realmResource.roles()).thenReturn(rolesResource);
|
||||
when(rolesResource.list()).thenReturn(Collections.emptyList());
|
||||
|
||||
long count = roleService.countUsersWithRole("non-existent-role", REALM, TypeRole.REALM_ROLE, null);
|
||||
|
||||
assertEquals(0, count);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCountUsersWithRole_Exception() {
|
||||
when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance);
|
||||
when(keycloakInstance.realm(REALM)).thenReturn(realmResource);
|
||||
when(realmResource.roles()).thenReturn(rolesResource);
|
||||
when(realmResource.users()).thenReturn(usersResource);
|
||||
|
||||
RoleRepresentation roleRep = new RoleRepresentation();
|
||||
roleRep.setId("role-123");
|
||||
roleRep.setName(ROLE_NAME);
|
||||
when(rolesResource.list()).thenReturn(List.of(roleRep));
|
||||
when(usersResource.list()).thenThrow(new RuntimeException("Error"));
|
||||
|
||||
long count = roleService.countUsersWithRole("role-123", REALM, TypeRole.REALM_ROLE, null);
|
||||
|
||||
assertEquals(0, count);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,589 @@
|
||||
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 org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.keycloak.admin.client.Keycloak;
|
||||
import org.keycloak.admin.client.resource.*;
|
||||
import org.keycloak.representations.idm.ClientRepresentation;
|
||||
import org.keycloak.representations.idm.RoleRepresentation;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.mockito.junit.jupiter.MockitoSettings;
|
||||
import org.mockito.quality.Strictness;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
* Tests d'intégration pour RoleServiceImpl - Cas limites et branches conditionnelles complexes
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@MockitoSettings(strictness = Strictness.LENIENT)
|
||||
class RoleServiceImplIntegrationTest {
|
||||
|
||||
@Mock
|
||||
private KeycloakAdminClient keycloakAdminClient;
|
||||
|
||||
@Mock
|
||||
private Keycloak keycloakInstance;
|
||||
|
||||
@Mock
|
||||
private RealmResource realmResource;
|
||||
|
||||
@Mock
|
||||
private RolesResource rolesResource;
|
||||
|
||||
@Mock
|
||||
private RoleResource roleResource;
|
||||
|
||||
@Mock
|
||||
private UsersResource usersResource;
|
||||
|
||||
@Mock
|
||||
private UserResource userResource;
|
||||
|
||||
@Mock
|
||||
private RoleMappingResource roleMappingResource;
|
||||
|
||||
@Mock
|
||||
private RoleScopeResource roleScopeResource;
|
||||
|
||||
@Mock
|
||||
private ClientsResource clientsResource;
|
||||
|
||||
@Mock
|
||||
private ClientResource clientResource;
|
||||
|
||||
@InjectMocks
|
||||
private RoleServiceImpl roleService;
|
||||
|
||||
private static final String REALM = "test-realm";
|
||||
private static final String USER_ID = "user-123";
|
||||
private static final String ROLE_NAME = "admin";
|
||||
private static final String CLIENT_NAME = "test-client";
|
||||
private static final String ROLE_ID = "role-123";
|
||||
|
||||
// ==================== Tests getRoleByName - Cas limites ====================
|
||||
|
||||
@Test
|
||||
void testGetRoleByName_RealmRole_Success() {
|
||||
when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance);
|
||||
when(keycloakInstance.realm(REALM)).thenReturn(realmResource);
|
||||
when(realmResource.roles()).thenReturn(rolesResource);
|
||||
when(rolesResource.get(ROLE_NAME)).thenReturn(roleResource);
|
||||
|
||||
RoleRepresentation roleRep = new RoleRepresentation();
|
||||
roleRep.setName(ROLE_NAME);
|
||||
roleRep.setId(ROLE_ID);
|
||||
when(roleResource.toRepresentation()).thenReturn(roleRep);
|
||||
|
||||
Optional<RoleDTO> result = roleService.getRoleByName(ROLE_NAME, REALM, TypeRole.REALM_ROLE, null);
|
||||
|
||||
assertTrue(result.isPresent());
|
||||
assertEquals(ROLE_NAME, result.get().getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetRoleByName_RealmRole_NotFound() {
|
||||
when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance);
|
||||
when(keycloakInstance.realm(REALM)).thenReturn(realmResource);
|
||||
when(realmResource.roles()).thenReturn(rolesResource);
|
||||
when(rolesResource.get(ROLE_NAME)).thenReturn(roleResource);
|
||||
when(roleResource.toRepresentation()).thenThrow(new jakarta.ws.rs.NotFoundException());
|
||||
|
||||
Optional<RoleDTO> result = roleService.getRoleByName(ROLE_NAME, REALM, TypeRole.REALM_ROLE, null);
|
||||
|
||||
assertFalse(result.isPresent());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetRoleByName_ClientRole_Success() {
|
||||
when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance);
|
||||
when(keycloakInstance.realm(REALM)).thenReturn(realmResource);
|
||||
when(realmResource.clients()).thenReturn(clientsResource);
|
||||
|
||||
ClientRepresentation client = new ClientRepresentation();
|
||||
client.setId("client-123");
|
||||
client.setClientId(CLIENT_NAME);
|
||||
when(clientsResource.findByClientId(CLIENT_NAME)).thenReturn(List.of(client));
|
||||
when(clientsResource.get("client-123")).thenReturn(clientResource);
|
||||
when(clientResource.roles()).thenReturn(rolesResource);
|
||||
when(rolesResource.get(ROLE_NAME)).thenReturn(roleResource);
|
||||
|
||||
RoleRepresentation roleRep = new RoleRepresentation();
|
||||
roleRep.setName(ROLE_NAME);
|
||||
roleRep.setId(ROLE_ID);
|
||||
when(roleResource.toRepresentation()).thenReturn(roleRep);
|
||||
|
||||
Optional<RoleDTO> result = roleService.getRoleByName(ROLE_NAME, REALM, TypeRole.CLIENT_ROLE, CLIENT_NAME);
|
||||
|
||||
assertTrue(result.isPresent());
|
||||
assertEquals(ROLE_NAME, result.get().getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetRoleByName_ClientRole_ClientNotFound() {
|
||||
when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance);
|
||||
when(keycloakInstance.realm(REALM)).thenReturn(realmResource);
|
||||
when(realmResource.clients()).thenReturn(clientsResource);
|
||||
when(clientsResource.findByClientId(CLIENT_NAME)).thenReturn(Collections.emptyList());
|
||||
|
||||
Optional<RoleDTO> result = roleService.getRoleByName(ROLE_NAME, REALM, TypeRole.CLIENT_ROLE, CLIENT_NAME);
|
||||
|
||||
assertFalse(result.isPresent());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetRoleByName_ClientRole_NullClientName() {
|
||||
Optional<RoleDTO> result = roleService.getRoleByName(ROLE_NAME, REALM, TypeRole.CLIENT_ROLE, null);
|
||||
|
||||
assertFalse(result.isPresent());
|
||||
}
|
||||
|
||||
// ==================== Tests assignRolesToUser - Cas limites ====================
|
||||
|
||||
@Test
|
||||
void testAssignRolesToUser_RealmRole_Success() {
|
||||
when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance);
|
||||
when(keycloakInstance.realm(REALM)).thenReturn(realmResource);
|
||||
when(realmResource.roles()).thenReturn(rolesResource);
|
||||
when(realmResource.users()).thenReturn(usersResource);
|
||||
when(usersResource.get(USER_ID)).thenReturn(userResource);
|
||||
when(userResource.roles()).thenReturn(roleMappingResource);
|
||||
when(roleMappingResource.realmLevel()).thenReturn(roleScopeResource);
|
||||
|
||||
RoleRepresentation roleRep = new RoleRepresentation();
|
||||
roleRep.setName(ROLE_NAME);
|
||||
when(rolesResource.get(ROLE_NAME)).thenReturn(roleResource);
|
||||
when(roleResource.toRepresentation()).thenReturn(roleRep);
|
||||
|
||||
RoleAssignmentDTO assignment = RoleAssignmentDTO.builder()
|
||||
.userId(USER_ID)
|
||||
.realmName(REALM)
|
||||
.typeRole(TypeRole.REALM_ROLE)
|
||||
.roleNames(List.of(ROLE_NAME))
|
||||
.build();
|
||||
|
||||
roleService.assignRolesToUser(assignment);
|
||||
|
||||
verify(roleScopeResource).add(anyList());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAssignRolesToUser_ClientRole_Success() {
|
||||
when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance);
|
||||
when(keycloakInstance.realm(REALM)).thenReturn(realmResource);
|
||||
when(realmResource.clients()).thenReturn(clientsResource);
|
||||
when(realmResource.users()).thenReturn(usersResource);
|
||||
when(usersResource.get(USER_ID)).thenReturn(userResource);
|
||||
when(userResource.roles()).thenReturn(roleMappingResource);
|
||||
|
||||
ClientRepresentation client = new ClientRepresentation();
|
||||
client.setId("client-123");
|
||||
when(clientsResource.findByClientId(CLIENT_NAME)).thenReturn(List.of(client));
|
||||
when(clientsResource.get("client-123")).thenReturn(clientResource);
|
||||
when(clientResource.roles()).thenReturn(rolesResource);
|
||||
when(rolesResource.get(ROLE_NAME)).thenReturn(roleResource);
|
||||
when(roleMappingResource.clientLevel("client-123")).thenReturn(roleScopeResource);
|
||||
|
||||
RoleRepresentation roleRep = new RoleRepresentation();
|
||||
roleRep.setName(ROLE_NAME);
|
||||
when(roleResource.toRepresentation()).thenReturn(roleRep);
|
||||
|
||||
RoleAssignmentDTO assignment = RoleAssignmentDTO.builder()
|
||||
.userId(USER_ID)
|
||||
.realmName(REALM)
|
||||
.typeRole(TypeRole.CLIENT_ROLE)
|
||||
.clientName(CLIENT_NAME)
|
||||
.roleNames(List.of(ROLE_NAME))
|
||||
.build();
|
||||
|
||||
roleService.assignRolesToUser(assignment);
|
||||
|
||||
verify(roleScopeResource).add(anyList());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAssignRolesToUser_ClientRole_NullClientName() {
|
||||
RoleAssignmentDTO assignment = RoleAssignmentDTO.builder()
|
||||
.userId(USER_ID)
|
||||
.realmName(REALM)
|
||||
.typeRole(TypeRole.CLIENT_ROLE)
|
||||
.clientName(null)
|
||||
.roleNames(List.of(ROLE_NAME))
|
||||
.build();
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> roleService.assignRolesToUser(assignment));
|
||||
}
|
||||
|
||||
// ==================== Tests revokeRolesFromUser - Cas limites ====================
|
||||
|
||||
@Test
|
||||
void testRevokeRolesFromUser_RealmRole_Success() {
|
||||
when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance);
|
||||
when(keycloakInstance.realm(REALM)).thenReturn(realmResource);
|
||||
when(realmResource.roles()).thenReturn(rolesResource);
|
||||
when(realmResource.users()).thenReturn(usersResource);
|
||||
when(usersResource.get(USER_ID)).thenReturn(userResource);
|
||||
when(userResource.roles()).thenReturn(roleMappingResource);
|
||||
when(roleMappingResource.realmLevel()).thenReturn(roleScopeResource);
|
||||
|
||||
RoleRepresentation roleRep = new RoleRepresentation();
|
||||
roleRep.setName(ROLE_NAME);
|
||||
when(rolesResource.get(ROLE_NAME)).thenReturn(roleResource);
|
||||
when(roleResource.toRepresentation()).thenReturn(roleRep);
|
||||
|
||||
RoleAssignmentDTO assignment = RoleAssignmentDTO.builder()
|
||||
.userId(USER_ID)
|
||||
.realmName(REALM)
|
||||
.typeRole(TypeRole.REALM_ROLE)
|
||||
.roleNames(List.of(ROLE_NAME))
|
||||
.build();
|
||||
|
||||
roleService.revokeRolesFromUser(assignment);
|
||||
|
||||
verify(roleScopeResource).remove(anyList());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRevokeRolesFromUser_ClientRole_NullClientName() {
|
||||
RoleAssignmentDTO assignment = RoleAssignmentDTO.builder()
|
||||
.userId(USER_ID)
|
||||
.realmName(REALM)
|
||||
.typeRole(TypeRole.CLIENT_ROLE)
|
||||
.clientName(null)
|
||||
.roleNames(List.of(ROLE_NAME))
|
||||
.build();
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> roleService.revokeRolesFromUser(assignment));
|
||||
}
|
||||
|
||||
// ==================== Tests getAllUserRoles - Cas limites ====================
|
||||
|
||||
@Test
|
||||
void testGetAllUserRoles_WithRealmAndClientRoles() {
|
||||
when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance);
|
||||
when(keycloakInstance.realm(REALM)).thenReturn(realmResource);
|
||||
when(realmResource.users()).thenReturn(usersResource);
|
||||
when(usersResource.get(USER_ID)).thenReturn(userResource);
|
||||
when(userResource.roles()).thenReturn(roleMappingResource);
|
||||
when(realmResource.clients()).thenReturn(clientsResource);
|
||||
|
||||
// Mock realm roles - getUserRealmRoles is called first
|
||||
RoleScopeResource realmRoleScope = mock(RoleScopeResource.class);
|
||||
RoleRepresentation realmRole = new RoleRepresentation();
|
||||
realmRole.setName("realm-role");
|
||||
when(roleMappingResource.realmLevel()).thenReturn(realmRoleScope);
|
||||
when(realmRoleScope.listAll()).thenReturn(List.of(realmRole));
|
||||
|
||||
// Mock client roles - getAllUserRoles calls getUserClientRoles for each client
|
||||
// getAllUserRoles calls getUserClientRoles with client.getClientId() (CLIENT_NAME)
|
||||
// getUserClientRoles then finds the client by clientId and uses the internal ID
|
||||
ClientRepresentation client = new ClientRepresentation();
|
||||
client.setId("client-123"); // Internal ID
|
||||
client.setClientId(CLIENT_NAME); // Client ID
|
||||
when(clientsResource.findAll()).thenReturn(List.of(client));
|
||||
|
||||
// getUserClientRoles finds client by clientId
|
||||
when(clientsResource.findByClientId(CLIENT_NAME)).thenReturn(List.of(client));
|
||||
|
||||
// getUserClientRoles uses internal ID for clientLevel
|
||||
RoleScopeResource clientRoleScope = mock(RoleScopeResource.class);
|
||||
when(roleMappingResource.clientLevel("client-123")).thenReturn(clientRoleScope);
|
||||
RoleRepresentation clientRole = new RoleRepresentation();
|
||||
clientRole.setName("client-role");
|
||||
when(clientRoleScope.listAll()).thenReturn(List.of(clientRole));
|
||||
|
||||
List<RoleDTO> result = roleService.getAllUserRoles(USER_ID, REALM);
|
||||
|
||||
assertNotNull(result);
|
||||
assertTrue(result.size() >= 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetAllUserRoles_WithExceptionInClientRoles() {
|
||||
when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance);
|
||||
when(keycloakInstance.realm(REALM)).thenReturn(realmResource);
|
||||
when(realmResource.users()).thenReturn(usersResource);
|
||||
when(usersResource.get(USER_ID)).thenReturn(userResource);
|
||||
when(userResource.roles()).thenReturn(roleMappingResource);
|
||||
when(roleMappingResource.realmLevel()).thenReturn(roleScopeResource);
|
||||
when(realmResource.clients()).thenReturn(clientsResource);
|
||||
|
||||
// Mock realm roles
|
||||
RoleRepresentation realmRole = new RoleRepresentation();
|
||||
realmRole.setName("realm-role");
|
||||
when(roleScopeResource.listAll()).thenReturn(List.of(realmRole));
|
||||
|
||||
// Exception when getting clients
|
||||
when(clientsResource.findAll()).thenThrow(new RuntimeException("Error"));
|
||||
|
||||
// Should not throw, just log warning
|
||||
List<RoleDTO> result = roleService.getAllUserRoles(USER_ID, REALM);
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals(1, result.size()); // Only realm roles
|
||||
}
|
||||
|
||||
// ==================== Tests addCompositeRoles - Cas limites ====================
|
||||
|
||||
@Test
|
||||
void testAddCompositeRoles_RealmRole_ParentNotFound() {
|
||||
when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance);
|
||||
when(keycloakInstance.realm(REALM)).thenReturn(realmResource);
|
||||
when(realmResource.roles()).thenReturn(rolesResource);
|
||||
when(rolesResource.list()).thenReturn(Collections.emptyList());
|
||||
|
||||
// getRoleById returns Optional.empty() when role not found, which causes NotFoundException
|
||||
assertThrows(jakarta.ws.rs.NotFoundException.class, () ->
|
||||
roleService.addCompositeRoles(ROLE_ID, List.of("child-1"), REALM, TypeRole.REALM_ROLE, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAddCompositeRoles_RealmRole_ChildNotFound() {
|
||||
when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance);
|
||||
when(keycloakInstance.realm(REALM)).thenReturn(realmResource);
|
||||
when(realmResource.roles()).thenReturn(rolesResource);
|
||||
|
||||
RoleRepresentation parentRole = new RoleRepresentation();
|
||||
parentRole.setId(ROLE_ID);
|
||||
parentRole.setName("parent");
|
||||
// Mock getRoleById to return parent role
|
||||
when(rolesResource.list()).thenReturn(List.of(parentRole));
|
||||
when(rolesResource.get("parent")).thenReturn(roleResource);
|
||||
when(roleResource.toRepresentation()).thenReturn(parentRole);
|
||||
|
||||
// Child role not found - getRealmRoleById returns empty for child
|
||||
// This means childRoleNames will be empty, so addComposites won't be called
|
||||
// Should not throw, just log warning and skip
|
||||
roleService.addCompositeRoles(ROLE_ID, List.of("child-id"), REALM, TypeRole.REALM_ROLE, null);
|
||||
|
||||
// Verify that get was called for parent role - use lenient to avoid unnecessary stubbing
|
||||
verify(rolesResource, atLeastOnce()).list();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAddCompositeRoles_ClientRole_ClientNotFound() {
|
||||
when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance);
|
||||
when(keycloakInstance.realm(REALM)).thenReturn(realmResource);
|
||||
when(realmResource.roles()).thenReturn(rolesResource);
|
||||
when(realmResource.clients()).thenReturn(clientsResource);
|
||||
|
||||
// Mock getRoleById to return a role
|
||||
RoleRepresentation parentRole = new RoleRepresentation();
|
||||
parentRole.setId(ROLE_ID);
|
||||
parentRole.setName("parent");
|
||||
when(rolesResource.list()).thenReturn(List.of(parentRole));
|
||||
when(rolesResource.get("parent")).thenReturn(roleResource);
|
||||
when(roleResource.toRepresentation()).thenReturn(parentRole);
|
||||
|
||||
when(clientsResource.findByClientId(CLIENT_NAME)).thenReturn(Collections.emptyList());
|
||||
|
||||
// When client not found, it throws IllegalArgumentException in removeCompositeRoles
|
||||
// But in addCompositeRoles, it first checks getRoleById which may throw NotFoundException
|
||||
// Actually, looking at the code, if client is not found, it throws IllegalArgumentException
|
||||
// But getRoleById might throw NotFoundException first
|
||||
assertThrows(jakarta.ws.rs.NotFoundException.class, () ->
|
||||
roleService.addCompositeRoles(ROLE_ID, List.of("child-1"), REALM, TypeRole.CLIENT_ROLE, CLIENT_NAME));
|
||||
}
|
||||
|
||||
// ==================== Tests removeCompositeRoles - Cas limites ====================
|
||||
|
||||
@Test
|
||||
void testRemoveCompositeRoles_RealmRole_ChildNotFound() {
|
||||
when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance);
|
||||
when(keycloakInstance.realm(REALM)).thenReturn(realmResource);
|
||||
when(realmResource.roles()).thenReturn(rolesResource);
|
||||
|
||||
RoleRepresentation parentRole = new RoleRepresentation();
|
||||
parentRole.setId(ROLE_ID);
|
||||
parentRole.setName("parent");
|
||||
when(rolesResource.list()).thenReturn(List.of(parentRole));
|
||||
|
||||
// Child role not found - getRealmRoleById returns empty, so childRoleNames will be empty
|
||||
// Should not throw, just log warning and skip
|
||||
roleService.removeCompositeRoles(ROLE_ID, List.of("child-id"), REALM, TypeRole.REALM_ROLE, null);
|
||||
|
||||
// Verify that list was called
|
||||
verify(rolesResource, atLeastOnce()).list();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRemoveCompositeRoles_ClientRole_ClientNotFound() {
|
||||
when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance);
|
||||
when(keycloakInstance.realm(REALM)).thenReturn(realmResource);
|
||||
when(realmResource.roles()).thenReturn(rolesResource);
|
||||
when(realmResource.clients()).thenReturn(clientsResource);
|
||||
|
||||
// Mock getRoleById to return a role
|
||||
RoleRepresentation parentRole = new RoleRepresentation();
|
||||
parentRole.setId(ROLE_ID);
|
||||
parentRole.setName("parent");
|
||||
when(rolesResource.list()).thenReturn(List.of(parentRole));
|
||||
when(rolesResource.get("parent")).thenReturn(roleResource);
|
||||
when(roleResource.toRepresentation()).thenReturn(parentRole);
|
||||
|
||||
when(clientsResource.findByClientId(CLIENT_NAME)).thenReturn(Collections.emptyList());
|
||||
|
||||
// When client not found, it throws IllegalArgumentException
|
||||
// But getRoleById might throw NotFoundException first
|
||||
assertThrows(jakarta.ws.rs.NotFoundException.class, () ->
|
||||
roleService.removeCompositeRoles(ROLE_ID, List.of("child-1"), REALM, TypeRole.CLIENT_ROLE, CLIENT_NAME));
|
||||
}
|
||||
|
||||
// ==================== Tests getAllRealmRoles - Cas limites ====================
|
||||
|
||||
@Test
|
||||
void testGetAllRealmRoles_RealmNotFound() {
|
||||
// realmExists returns false, so it throws IllegalArgumentException
|
||||
when(keycloakAdminClient.realmExists(REALM)).thenReturn(false);
|
||||
|
||||
// But if realmExists throws an exception, it might be wrapped
|
||||
// Let's test both cases
|
||||
try {
|
||||
roleService.getAllRealmRoles(REALM);
|
||||
fail("Should have thrown an exception");
|
||||
} catch (IllegalArgumentException e) {
|
||||
// Expected when realmExists returns false
|
||||
assertTrue(e.getMessage().contains("n'existe pas"));
|
||||
} catch (RuntimeException e) {
|
||||
// Also possible if realmExists throws
|
||||
assertTrue(e.getMessage().contains("n'existe pas") ||
|
||||
e.getMessage().contains("récupération des rôles realm"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetAllRealmRoles_NotFoundException() {
|
||||
when(keycloakAdminClient.realmExists(REALM)).thenReturn(true);
|
||||
when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance);
|
||||
when(keycloakInstance.realm(REALM)).thenReturn(realmResource);
|
||||
when(realmResource.roles()).thenReturn(rolesResource);
|
||||
when(rolesResource.list()).thenThrow(new jakarta.ws.rs.NotFoundException());
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () ->
|
||||
roleService.getAllRealmRoles(REALM));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetAllRealmRoles_ExceptionWith404() {
|
||||
when(keycloakAdminClient.realmExists(REALM)).thenReturn(true);
|
||||
when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance);
|
||||
when(keycloakInstance.realm(REALM)).thenReturn(realmResource);
|
||||
when(realmResource.roles()).thenReturn(rolesResource);
|
||||
when(rolesResource.list()).thenThrow(new RuntimeException("Server response is: 404"));
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () ->
|
||||
roleService.getAllRealmRoles(REALM));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetAllRealmRoles_ExceptionWithNotFound() {
|
||||
when(keycloakAdminClient.realmExists(REALM)).thenReturn(true);
|
||||
when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance);
|
||||
when(keycloakInstance.realm(REALM)).thenReturn(realmResource);
|
||||
when(realmResource.roles()).thenReturn(rolesResource);
|
||||
when(rolesResource.list()).thenThrow(new RuntimeException("Not Found"));
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () ->
|
||||
roleService.getAllRealmRoles(REALM));
|
||||
}
|
||||
|
||||
// ==================== Tests getAllClientRoles - Cas limites ====================
|
||||
|
||||
@Test
|
||||
void testGetAllClientRoles_ClientNotFound() {
|
||||
when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance);
|
||||
when(keycloakInstance.realm(REALM)).thenReturn(realmResource);
|
||||
when(realmResource.clients()).thenReturn(clientsResource);
|
||||
when(clientsResource.findByClientId(CLIENT_NAME)).thenReturn(Collections.emptyList());
|
||||
|
||||
List<RoleDTO> result = roleService.getAllClientRoles(REALM, CLIENT_NAME);
|
||||
|
||||
assertNotNull(result);
|
||||
assertTrue(result.isEmpty());
|
||||
}
|
||||
|
||||
// ==================== Tests createClientRole - Cas limites ====================
|
||||
|
||||
@Test
|
||||
void testCreateClientRole_ClientNotFound() {
|
||||
when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance);
|
||||
when(keycloakInstance.realm(REALM)).thenReturn(realmResource);
|
||||
when(realmResource.clients()).thenReturn(clientsResource);
|
||||
when(clientsResource.findByClientId(CLIENT_NAME)).thenReturn(Collections.emptyList());
|
||||
|
||||
RoleDTO roleDTO = RoleDTO.builder()
|
||||
.name(ROLE_NAME)
|
||||
.build();
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () ->
|
||||
roleService.createClientRole(roleDTO, REALM, CLIENT_NAME));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreateClientRole_RoleAlreadyExists() {
|
||||
when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance);
|
||||
when(keycloakInstance.realm(REALM)).thenReturn(realmResource);
|
||||
when(realmResource.clients()).thenReturn(clientsResource);
|
||||
|
||||
ClientRepresentation client = new ClientRepresentation();
|
||||
client.setId("client-123");
|
||||
when(clientsResource.findByClientId(CLIENT_NAME)).thenReturn(List.of(client));
|
||||
when(clientsResource.get("client-123")).thenReturn(clientResource);
|
||||
when(clientResource.roles()).thenReturn(rolesResource);
|
||||
when(rolesResource.get(ROLE_NAME)).thenReturn(roleResource);
|
||||
when(roleResource.toRepresentation()).thenReturn(new RoleRepresentation());
|
||||
|
||||
RoleDTO roleDTO = RoleDTO.builder()
|
||||
.name(ROLE_NAME)
|
||||
.build();
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () ->
|
||||
roleService.createClientRole(roleDTO, REALM, CLIENT_NAME));
|
||||
}
|
||||
|
||||
// ==================== Tests countUsersWithRole - Cas limites ====================
|
||||
|
||||
@Test
|
||||
void testCountUsersWithRole_RoleNotFound() {
|
||||
when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance);
|
||||
when(keycloakInstance.realm(REALM)).thenReturn(realmResource);
|
||||
when(realmResource.roles()).thenReturn(rolesResource);
|
||||
when(rolesResource.list()).thenReturn(Collections.emptyList());
|
||||
|
||||
long count = roleService.countUsersWithRole(ROLE_ID, REALM, TypeRole.REALM_ROLE, null);
|
||||
|
||||
assertEquals(0, count);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCountUsersWithRole_Exception() {
|
||||
when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance);
|
||||
when(keycloakInstance.realm(REALM)).thenReturn(realmResource);
|
||||
when(realmResource.roles()).thenReturn(rolesResource);
|
||||
when(realmResource.users()).thenReturn(usersResource);
|
||||
|
||||
RoleRepresentation roleRep = new RoleRepresentation();
|
||||
roleRep.setId(ROLE_ID);
|
||||
roleRep.setName(ROLE_NAME);
|
||||
when(rolesResource.list()).thenReturn(List.of(roleRep));
|
||||
when(usersResource.list()).thenThrow(new RuntimeException("Error"));
|
||||
|
||||
long count = roleService.countUsersWithRole(ROLE_ID, REALM, TypeRole.REALM_ROLE, null);
|
||||
|
||||
assertEquals(0, count); // Should return 0 on exception
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
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 org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.keycloak.admin.client.Keycloak;
|
||||
import org.keycloak.admin.client.resource.*;
|
||||
import org.keycloak.representations.idm.RoleRepresentation;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class RoleServiceImplTest {
|
||||
|
||||
@Mock
|
||||
KeycloakAdminClient keycloakAdminClient;
|
||||
|
||||
@Mock
|
||||
Keycloak keycloakInstance;
|
||||
|
||||
@Mock
|
||||
RealmResource realmResource;
|
||||
|
||||
@Mock
|
||||
RolesResource rolesResource;
|
||||
|
||||
@Mock
|
||||
RoleResource roleResource;
|
||||
|
||||
@Mock
|
||||
UsersResource usersResource;
|
||||
|
||||
@Mock
|
||||
UserResource userResource;
|
||||
|
||||
@Mock
|
||||
RoleMappingResource roleMappingResource;
|
||||
|
||||
@Mock
|
||||
RoleScopeResource roleScopeResource;
|
||||
|
||||
@InjectMocks
|
||||
RoleServiceImpl roleService;
|
||||
|
||||
private static final String REALM = "test-realm";
|
||||
|
||||
@Test
|
||||
void testCreateRealmRole() {
|
||||
when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance);
|
||||
when(keycloakInstance.realm(REALM)).thenReturn(realmResource);
|
||||
when(realmResource.roles()).thenReturn(rolesResource);
|
||||
|
||||
// Check not found initially, then return created role
|
||||
RoleRepresentation createdRep = new RoleRepresentation();
|
||||
createdRep.setName("role");
|
||||
createdRep.setId("1");
|
||||
when(rolesResource.get("role")).thenReturn(roleResource);
|
||||
when(roleResource.toRepresentation()).thenThrow(new jakarta.ws.rs.NotFoundException())
|
||||
.thenReturn(createdRep);
|
||||
|
||||
// Mock create
|
||||
doNothing().when(rolesResource).create(any(RoleRepresentation.class));
|
||||
|
||||
RoleDTO input = RoleDTO.builder().name("role").description("desc").build();
|
||||
|
||||
RoleDTO result = roleService.createRealmRole(input, REALM);
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals("role", result.getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDeleteRole() {
|
||||
when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance);
|
||||
when(keycloakInstance.realm(REALM)).thenReturn(realmResource);
|
||||
when(realmResource.roles()).thenReturn(rolesResource);
|
||||
|
||||
// find by id logic uses list()
|
||||
RoleRepresentation rep = new RoleRepresentation();
|
||||
rep.setId("1");
|
||||
rep.setName("role");
|
||||
when(rolesResource.list()).thenReturn(Collections.singletonList(rep));
|
||||
|
||||
roleService.deleteRole("1", REALM, TypeRole.REALM_ROLE, null);
|
||||
|
||||
verify(rolesResource).deleteRole("role");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAssignRolesToUser() {
|
||||
when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance);
|
||||
when(keycloakInstance.realm(REALM)).thenReturn(realmResource);
|
||||
when(realmResource.roles()).thenReturn(rolesResource);
|
||||
when(realmResource.users()).thenReturn(usersResource);
|
||||
when(usersResource.get("u1")).thenReturn(userResource);
|
||||
when(userResource.roles()).thenReturn(roleMappingResource);
|
||||
when(roleMappingResource.realmLevel()).thenReturn(roleScopeResource);
|
||||
|
||||
RoleRepresentation roleRep = new RoleRepresentation();
|
||||
roleRep.setName("role1");
|
||||
when(rolesResource.get("role1")).thenReturn(roleResource);
|
||||
when(roleResource.toRepresentation()).thenReturn(roleRep);
|
||||
|
||||
RoleAssignmentDTO assignment = RoleAssignmentDTO.builder()
|
||||
.userId("u1")
|
||||
.realmName(REALM)
|
||||
.typeRole(TypeRole.REALM_ROLE)
|
||||
.roleNames(Collections.singletonList("role1"))
|
||||
.build();
|
||||
|
||||
roleService.assignRolesToUser(assignment);
|
||||
|
||||
verify(roleScopeResource).add(anyList());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
package dev.lions.user.manager.service.impl;
|
||||
|
||||
import dev.lions.user.manager.client.KeycloakAdminClient;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.keycloak.admin.client.Keycloak;
|
||||
import org.keycloak.admin.client.resource.*;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.representations.idm.RoleRepresentation;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.keycloak.representations.info.ServerInfoRepresentation;
|
||||
import org.keycloak.representations.info.SystemInfoRepresentation; // Correct import
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import org.mockito.quality.Strictness;
|
||||
import org.mockito.junit.jupiter.MockitoSettings;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@MockitoSettings(strictness = Strictness.LENIENT)
|
||||
class SyncServiceImplTest {
|
||||
|
||||
@Mock
|
||||
KeycloakAdminClient keycloakAdminClient;
|
||||
|
||||
@Mock
|
||||
Keycloak keycloakInstance;
|
||||
|
||||
@Mock
|
||||
RealmsResource realmsResource;
|
||||
|
||||
@Mock
|
||||
RealmResource realmResource;
|
||||
|
||||
@Mock
|
||||
UsersResource usersResource;
|
||||
|
||||
@Mock
|
||||
RolesResource rolesResource;
|
||||
|
||||
@Mock
|
||||
ServerInfoResource serverInfoResource;
|
||||
|
||||
@Mock
|
||||
dev.lions.user.manager.server.impl.repository.SyncHistoryRepository syncHistoryRepository;
|
||||
|
||||
@Mock
|
||||
dev.lions.user.manager.server.impl.repository.SyncedUserRepository syncedUserRepository;
|
||||
|
||||
@Mock
|
||||
dev.lions.user.manager.server.impl.repository.SyncedRoleRepository syncedRoleRepository;
|
||||
|
||||
@InjectMocks
|
||||
SyncServiceImpl syncService;
|
||||
|
||||
// Correcting inner class usage if needed, but assuming standard Keycloak
|
||||
// representations
|
||||
// ServerInfoRepresentation contains SystemInfoRepresentation
|
||||
|
||||
@Test
|
||||
void testSyncUsersFromRealm() {
|
||||
when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance);
|
||||
when(keycloakInstance.realm("realm")).thenReturn(realmResource);
|
||||
when(realmResource.users()).thenReturn(usersResource);
|
||||
when(usersResource.list()).thenReturn(Collections.singletonList(new UserRepresentation()));
|
||||
|
||||
int count = syncService.syncUsersFromRealm("realm");
|
||||
assertEquals(1, count);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSyncRolesFromRealm() {
|
||||
when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance);
|
||||
when(keycloakInstance.realm("realm")).thenReturn(realmResource);
|
||||
when(realmResource.roles()).thenReturn(rolesResource);
|
||||
when(rolesResource.list()).thenReturn(Collections.singletonList(new RoleRepresentation()));
|
||||
|
||||
int count = syncService.syncRolesFromRealm("realm");
|
||||
assertEquals(1, count);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSyncAllRealms() {
|
||||
when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance);
|
||||
when(keycloakInstance.realms()).thenReturn(realmsResource);
|
||||
|
||||
RealmRepresentation realmRep = new RealmRepresentation();
|
||||
realmRep.setRealm("realm1");
|
||||
when(realmsResource.findAll()).thenReturn(Collections.singletonList(realmRep));
|
||||
|
||||
// Sync logic calls realm() again
|
||||
when(keycloakInstance.realm("realm1")).thenReturn(realmResource);
|
||||
when(realmResource.users()).thenReturn(usersResource);
|
||||
when(usersResource.list()).thenReturn(Collections.emptyList());
|
||||
when(realmResource.roles()).thenReturn(rolesResource);
|
||||
when(rolesResource.list()).thenReturn(Collections.emptyList());
|
||||
|
||||
Map<String, Integer> result = syncService.syncAllRealms();
|
||||
assertTrue(result.containsKey("realm1"));
|
||||
assertEquals(0, result.get("realm1"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testIsKeycloakAvailable() {
|
||||
when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance);
|
||||
when(keycloakInstance.serverInfo()).thenReturn(serverInfoResource);
|
||||
when(serverInfoResource.getInfo()).thenReturn(new ServerInfoRepresentation());
|
||||
|
||||
assertTrue(syncService.isKeycloakAvailable());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetKeycloakHealthInfo() {
|
||||
when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance);
|
||||
when(keycloakInstance.serverInfo()).thenReturn(serverInfoResource);
|
||||
|
||||
ServerInfoRepresentation info = new ServerInfoRepresentation();
|
||||
SystemInfoRepresentation systemInfo = new SystemInfoRepresentation();
|
||||
systemInfo.setVersion("1.0");
|
||||
info.setSystemInfo(systemInfo);
|
||||
|
||||
when(serverInfoResource.getInfo()).thenReturn(info);
|
||||
|
||||
Map<String, Object> health = syncService.getKeycloakHealthInfo();
|
||||
assertTrue((Boolean) health.get("overallHealthy"));
|
||||
assertEquals("1.0", health.get("keycloakVersion"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSyncUsersFromRealm_Exception() {
|
||||
when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance);
|
||||
when(keycloakInstance.realm("realm")).thenReturn(realmResource);
|
||||
when(realmResource.users()).thenReturn(usersResource);
|
||||
when(usersResource.list()).thenThrow(new RuntimeException("Connection error"));
|
||||
|
||||
assertThrows(RuntimeException.class, () -> syncService.syncUsersFromRealm("realm"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSyncRolesFromRealm_Exception() {
|
||||
when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance);
|
||||
when(keycloakInstance.realm("realm")).thenReturn(realmResource);
|
||||
when(realmResource.roles()).thenReturn(rolesResource);
|
||||
when(rolesResource.list()).thenThrow(new RuntimeException("Connection error"));
|
||||
|
||||
assertThrows(RuntimeException.class, () -> syncService.syncRolesFromRealm("realm"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSyncAllRealms_WithException() {
|
||||
when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance);
|
||||
when(keycloakInstance.realms()).thenReturn(realmsResource);
|
||||
|
||||
RealmRepresentation realmRep = new RealmRepresentation();
|
||||
realmRep.setRealm("realm1");
|
||||
when(realmsResource.findAll()).thenReturn(Collections.singletonList(realmRep));
|
||||
|
||||
// Mock exception during sync
|
||||
when(keycloakInstance.realm("realm1")).thenReturn(realmResource);
|
||||
when(realmResource.users()).thenReturn(usersResource);
|
||||
when(usersResource.list()).thenThrow(new RuntimeException("Sync error"));
|
||||
|
||||
Map<String, Integer> result = syncService.syncAllRealms();
|
||||
assertTrue(result.containsKey("realm1"));
|
||||
assertEquals(0, result.get("realm1")); // Should be 0 on error
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSyncAllRealms_ExceptionInFindAll() {
|
||||
when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance);
|
||||
when(keycloakInstance.realms()).thenReturn(realmsResource);
|
||||
when(realmsResource.findAll()).thenThrow(new RuntimeException("Connection error"));
|
||||
|
||||
Map<String, Integer> result = syncService.syncAllRealms();
|
||||
assertTrue(result.isEmpty());
|
||||
}
|
||||
|
||||
// Note: checkDataConsistency doesn't actually throw exceptions in the current
|
||||
// implementation
|
||||
// The try-catch block is there for future use, but currently always succeeds
|
||||
// So we test the success path in testCheckDataConsistency_Success
|
||||
|
||||
@Test
|
||||
void testForceSyncRealm_Exception() {
|
||||
when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance);
|
||||
when(keycloakInstance.realm("realm")).thenReturn(realmResource);
|
||||
when(realmResource.users()).thenReturn(usersResource);
|
||||
when(usersResource.list()).thenThrow(new RuntimeException("Sync error"));
|
||||
|
||||
Map<String, Object> stats = syncService.forceSyncRealm("realm");
|
||||
assertFalse((Boolean) stats.get("success"));
|
||||
assertNotNull(stats.get("error"));
|
||||
assertNotNull(stats.get("durationMs"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testIsKeycloakAvailable_Exception() {
|
||||
when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance);
|
||||
when(keycloakInstance.serverInfo()).thenReturn(serverInfoResource);
|
||||
when(serverInfoResource.getInfo()).thenThrow(new RuntimeException("Connection refused"));
|
||||
|
||||
assertFalse(syncService.isKeycloakAvailable());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetKeycloakHealthInfo_Exception() {
|
||||
when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance);
|
||||
when(keycloakInstance.serverInfo()).thenReturn(serverInfoResource);
|
||||
when(serverInfoResource.getInfo()).thenThrow(new RuntimeException("Connection error"));
|
||||
|
||||
Map<String, Object> health = syncService.getKeycloakHealthInfo();
|
||||
assertFalse((Boolean) health.get("overallHealthy"));
|
||||
assertFalse((Boolean) health.get("keycloakAccessible"));
|
||||
assertNotNull(health.get("errorMessage"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCheckDataConsistency_Success() {
|
||||
when(keycloakAdminClient.getInstance()).thenReturn(keycloakInstance);
|
||||
when(keycloakInstance.realm("realm")).thenReturn(realmResource);
|
||||
when(realmResource.users()).thenReturn(usersResource);
|
||||
when(usersResource.list()).thenReturn(Collections.emptyList());
|
||||
when(realmResource.roles()).thenReturn(rolesResource);
|
||||
when(rolesResource.list()).thenReturn(Collections.emptyList());
|
||||
|
||||
when(syncedUserRepository.list(anyString(), anyString())).thenReturn(Collections.emptyList());
|
||||
when(syncedRoleRepository.list(anyString(), anyString())).thenReturn(Collections.emptyList());
|
||||
|
||||
Map<String, Object> report = syncService.checkDataConsistency("realm");
|
||||
|
||||
assertEquals("realm", report.get("realmName"));
|
||||
assertEquals("OK", report.get("status"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetLastSyncStatus() {
|
||||
Map<String, Object> status = syncService.getLastSyncStatus("realm");
|
||||
assertEquals("realm", status.get("realmName"));
|
||||
assertEquals("completed", status.get("status"));
|
||||
assertNotNull(status.get("lastSyncTime"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,318 @@
|
||||
package dev.lions.user.manager.service.impl;
|
||||
|
||||
import dev.lions.user.manager.client.KeycloakAdminClient;
|
||||
import dev.lions.user.manager.dto.user.UserDTO;
|
||||
import dev.lions.user.manager.dto.user.UserSearchCriteriaDTO;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.keycloak.admin.client.resource.UserResource;
|
||||
import org.keycloak.admin.client.resource.UsersResource;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
* Tests complets pour UserServiceImpl pour atteindre 100% de couverture
|
||||
* Couvre les branches manquantes : filterUsers, searchUsers avec différents critères, etc.
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class UserServiceImplCompleteTest {
|
||||
|
||||
private static final String REALM = "test-realm";
|
||||
|
||||
@Mock
|
||||
private KeycloakAdminClient keycloakAdminClient;
|
||||
|
||||
@Mock
|
||||
private UsersResource usersResource;
|
||||
|
||||
@InjectMocks
|
||||
private UserServiceImpl userService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
when(keycloakAdminClient.getUsers(REALM)).thenReturn(usersResource);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSearchUsers_WithSearchTerm() {
|
||||
UserRepresentation user = new UserRepresentation();
|
||||
user.setUsername("testuser");
|
||||
user.setEnabled(true);
|
||||
when(usersResource.search("test", 0, 10)).thenReturn(List.of(user));
|
||||
when(usersResource.count()).thenReturn(1);
|
||||
|
||||
UserSearchCriteriaDTO criteria = UserSearchCriteriaDTO.builder()
|
||||
.realmName(REALM)
|
||||
.searchTerm("test")
|
||||
.page(0)
|
||||
.pageSize(10)
|
||||
.build();
|
||||
|
||||
var result = userService.searchUsers(criteria);
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals(1, result.getUsers().size());
|
||||
verify(usersResource).search("test", 0, 10);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSearchUsers_WithUsername() {
|
||||
UserRepresentation user = new UserRepresentation();
|
||||
user.setUsername("testuser");
|
||||
user.setEnabled(true);
|
||||
when(usersResource.search("testuser", 0, 10, true)).thenReturn(List.of(user));
|
||||
when(usersResource.count()).thenReturn(1);
|
||||
|
||||
UserSearchCriteriaDTO criteria = UserSearchCriteriaDTO.builder()
|
||||
.realmName(REALM)
|
||||
.username("testuser")
|
||||
.page(0)
|
||||
.pageSize(10)
|
||||
.build();
|
||||
|
||||
var result = userService.searchUsers(criteria);
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals(1, result.getUsers().size());
|
||||
verify(usersResource).search("testuser", 0, 10, true);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSearchUsers_WithEmail() {
|
||||
UserRepresentation user = new UserRepresentation();
|
||||
user.setUsername("testuser");
|
||||
user.setEmail("test@example.com");
|
||||
user.setEnabled(true);
|
||||
when(usersResource.searchByEmail("test@example.com", true)).thenReturn(List.of(user));
|
||||
when(usersResource.count()).thenReturn(1);
|
||||
|
||||
UserSearchCriteriaDTO criteria = UserSearchCriteriaDTO.builder()
|
||||
.realmName(REALM)
|
||||
.email("test@example.com")
|
||||
.page(0)
|
||||
.pageSize(10)
|
||||
.build();
|
||||
|
||||
var result = userService.searchUsers(criteria);
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals(1, result.getUsers().size());
|
||||
verify(usersResource).searchByEmail("test@example.com", true);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSearchUsers_ListAll() {
|
||||
UserRepresentation user = new UserRepresentation();
|
||||
user.setUsername("testuser");
|
||||
user.setEnabled(true);
|
||||
when(usersResource.list(0, 10)).thenReturn(List.of(user));
|
||||
when(usersResource.count()).thenReturn(1);
|
||||
|
||||
UserSearchCriteriaDTO criteria = UserSearchCriteriaDTO.builder()
|
||||
.realmName(REALM)
|
||||
.page(0)
|
||||
.pageSize(10)
|
||||
.build();
|
||||
|
||||
var result = userService.searchUsers(criteria);
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals(1, result.getUsers().size());
|
||||
verify(usersResource).list(0, 10);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSearchUsers_WithEnabledFilter() {
|
||||
UserRepresentation enabledUser = new UserRepresentation();
|
||||
enabledUser.setUsername("enabled");
|
||||
enabledUser.setEnabled(true);
|
||||
UserRepresentation disabledUser = new UserRepresentation();
|
||||
disabledUser.setUsername("disabled");
|
||||
disabledUser.setEnabled(false);
|
||||
|
||||
when(usersResource.list(0, 10)).thenReturn(List.of(enabledUser, disabledUser));
|
||||
when(usersResource.count()).thenReturn(2);
|
||||
|
||||
UserSearchCriteriaDTO criteria = UserSearchCriteriaDTO.builder()
|
||||
.realmName(REALM)
|
||||
.enabled(true)
|
||||
.page(0)
|
||||
.pageSize(10)
|
||||
.build();
|
||||
|
||||
var result = userService.searchUsers(criteria);
|
||||
|
||||
assertNotNull(result);
|
||||
// Seul l'utilisateur activé devrait être retourné
|
||||
assertEquals(1, result.getUsers().size());
|
||||
assertTrue(result.getUsers().get(0).getEnabled());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSearchUsers_WithEmailVerifiedFilter() {
|
||||
UserRepresentation verifiedUser = new UserRepresentation();
|
||||
verifiedUser.setUsername("verified");
|
||||
verifiedUser.setEmailVerified(true);
|
||||
verifiedUser.setEnabled(true);
|
||||
UserRepresentation unverifiedUser = new UserRepresentation();
|
||||
unverifiedUser.setUsername("unverified");
|
||||
unverifiedUser.setEmailVerified(false);
|
||||
unverifiedUser.setEnabled(true);
|
||||
|
||||
when(usersResource.list(0, 10)).thenReturn(List.of(verifiedUser, unverifiedUser));
|
||||
when(usersResource.count()).thenReturn(2);
|
||||
|
||||
UserSearchCriteriaDTO criteria = UserSearchCriteriaDTO.builder()
|
||||
.realmName(REALM)
|
||||
.emailVerified(true)
|
||||
.page(0)
|
||||
.pageSize(10)
|
||||
.build();
|
||||
|
||||
var result = userService.searchUsers(criteria);
|
||||
|
||||
assertNotNull(result);
|
||||
// Seul l'utilisateur avec email vérifié devrait être retourné
|
||||
assertEquals(1, result.getUsers().size());
|
||||
assertTrue(result.getUsers().get(0).getEmailVerified());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSearchUsers_WithBlankSearchTerm() {
|
||||
UserRepresentation user = new UserRepresentation();
|
||||
user.setUsername("testuser");
|
||||
user.setEnabled(true);
|
||||
when(usersResource.list(0, 10)).thenReturn(List.of(user));
|
||||
when(usersResource.count()).thenReturn(1);
|
||||
|
||||
UserSearchCriteriaDTO criteria = UserSearchCriteriaDTO.builder()
|
||||
.realmName(REALM)
|
||||
.searchTerm(" ") // Blank search term
|
||||
.page(0)
|
||||
.pageSize(10)
|
||||
.build();
|
||||
|
||||
var result = userService.searchUsers(criteria);
|
||||
|
||||
assertNotNull(result);
|
||||
// Devrait utiliser list() au lieu de search() pour un terme vide
|
||||
verify(usersResource).list(0, 10);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testUpdateUser_WithAllFields() {
|
||||
UserResource userResource = mock(UserResource.class);
|
||||
when(usersResource.get("user-id")).thenReturn(userResource);
|
||||
|
||||
UserRepresentation existingUser = new UserRepresentation();
|
||||
existingUser.setId("user-id");
|
||||
existingUser.setUsername("olduser");
|
||||
existingUser.setEnabled(true);
|
||||
when(userResource.toRepresentation()).thenReturn(existingUser);
|
||||
|
||||
UserDTO userDTO = UserDTO.builder()
|
||||
.email("new@example.com")
|
||||
.prenom("John")
|
||||
.nom("Doe")
|
||||
.enabled(false)
|
||||
.emailVerified(true)
|
||||
.attributes(java.util.Map.of("key", java.util.List.of("value")))
|
||||
.build();
|
||||
|
||||
UserRepresentation updatedUser = new UserRepresentation();
|
||||
updatedUser.setId("user-id");
|
||||
updatedUser.setUsername("olduser");
|
||||
updatedUser.setEmail("new@example.com");
|
||||
updatedUser.setEnabled(true);
|
||||
when(userResource.toRepresentation()).thenReturn(existingUser, updatedUser);
|
||||
|
||||
UserDTO result = userService.updateUser("user-id", userDTO, REALM);
|
||||
|
||||
assertNotNull(result);
|
||||
verify(userResource).update(any(UserRepresentation.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testUpdateUser_WithNullFields() {
|
||||
UserResource userResource = mock(UserResource.class);
|
||||
when(usersResource.get("user-id")).thenReturn(userResource);
|
||||
|
||||
UserRepresentation existingUser = new UserRepresentation();
|
||||
existingUser.setId("user-id");
|
||||
existingUser.setUsername("olduser");
|
||||
existingUser.setEnabled(true);
|
||||
when(userResource.toRepresentation()).thenReturn(existingUser);
|
||||
|
||||
UserDTO userDTO = UserDTO.builder()
|
||||
.email(null)
|
||||
.prenom(null)
|
||||
.nom(null)
|
||||
.enabled(null)
|
||||
.emailVerified(null)
|
||||
.attributes(null)
|
||||
.build();
|
||||
|
||||
UserRepresentation updatedUser = new UserRepresentation();
|
||||
updatedUser.setId("user-id");
|
||||
updatedUser.setUsername("olduser");
|
||||
updatedUser.setEnabled(true);
|
||||
when(userResource.toRepresentation()).thenReturn(existingUser, updatedUser);
|
||||
|
||||
UserDTO result = userService.updateUser("user-id", userDTO, REALM);
|
||||
|
||||
assertNotNull(result);
|
||||
verify(userResource).update(any(UserRepresentation.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDeleteUser_HardDelete() {
|
||||
UserResource userResource = mock(UserResource.class);
|
||||
when(usersResource.get("user-id")).thenReturn(userResource);
|
||||
|
||||
userService.deleteUser("user-id", REALM, true);
|
||||
|
||||
verify(userResource).remove();
|
||||
verify(userResource, never()).update(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDeleteUser_SoftDelete() {
|
||||
UserResource userResource = mock(UserResource.class);
|
||||
when(usersResource.get("user-id")).thenReturn(userResource);
|
||||
|
||||
UserRepresentation user = new UserRepresentation();
|
||||
user.setId("user-id");
|
||||
user.setEnabled(true);
|
||||
when(userResource.toRepresentation()).thenReturn(user);
|
||||
|
||||
userService.deleteUser("user-id", REALM, false);
|
||||
|
||||
verify(userResource, never()).remove();
|
||||
verify(userResource).update(any(UserRepresentation.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSearchUsers_Exception() {
|
||||
when(usersResource.list(0, 10)).thenThrow(new RuntimeException("Connection error"));
|
||||
|
||||
UserSearchCriteriaDTO criteria = UserSearchCriteriaDTO.builder()
|
||||
.realmName(REALM)
|
||||
.page(0)
|
||||
.pageSize(10)
|
||||
.build();
|
||||
|
||||
assertThrows(RuntimeException.class, () ->
|
||||
userService.searchUsers(criteria));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,535 @@
|
||||
package dev.lions.user.manager.service.impl;
|
||||
|
||||
import dev.lions.user.manager.client.KeycloakAdminClient;
|
||||
import dev.lions.user.manager.dto.user.UserDTO;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.keycloak.admin.client.resource.RoleMappingResource;
|
||||
import org.keycloak.admin.client.resource.RoleScopeResource;
|
||||
import org.keycloak.admin.client.resource.UserResource;
|
||||
import org.keycloak.admin.client.resource.UsersResource;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
* Tests supplémentaires pour UserServiceImpl pour améliorer la couverture
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class UserServiceImplExtendedTest {
|
||||
|
||||
@Mock
|
||||
private KeycloakAdminClient keycloakAdminClient;
|
||||
|
||||
@Mock
|
||||
private UsersResource usersResource;
|
||||
|
||||
@Mock
|
||||
private UserResource userResource;
|
||||
|
||||
@Mock
|
||||
private RoleMappingResource roleMappingResource;
|
||||
|
||||
@Mock
|
||||
private RoleScopeResource roleScopeResource;
|
||||
|
||||
@InjectMocks
|
||||
private UserServiceImpl userService;
|
||||
|
||||
private static final String REALM = "test-realm";
|
||||
private static final String USER_ID = "user-123";
|
||||
|
||||
@Test
|
||||
void testDeactivateUser() {
|
||||
when(keycloakAdminClient.getUsers(REALM)).thenReturn(usersResource);
|
||||
when(usersResource.get(USER_ID)).thenReturn(userResource);
|
||||
|
||||
UserRepresentation userRep = new UserRepresentation();
|
||||
userRep.setId(USER_ID);
|
||||
userRep.setEnabled(true);
|
||||
when(userResource.toRepresentation()).thenReturn(userRep);
|
||||
|
||||
userService.deactivateUser(USER_ID, REALM, "Test reason");
|
||||
|
||||
verify(userResource).update(argThat(rep -> !rep.isEnabled()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testResetPassword() {
|
||||
when(keycloakAdminClient.getUsers(REALM)).thenReturn(usersResource);
|
||||
when(usersResource.get(USER_ID)).thenReturn(userResource);
|
||||
|
||||
userService.resetPassword(USER_ID, REALM, "newPassword123", true);
|
||||
|
||||
verify(userResource).resetPassword(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSendVerificationEmail() {
|
||||
when(keycloakAdminClient.getUsers(REALM)).thenReturn(usersResource);
|
||||
when(usersResource.get(USER_ID)).thenReturn(userResource);
|
||||
|
||||
userService.sendVerificationEmail(USER_ID, REALM);
|
||||
|
||||
verify(userResource).sendVerifyEmail();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLogoutAllSessions() {
|
||||
when(keycloakAdminClient.getUsers(REALM)).thenReturn(usersResource);
|
||||
when(usersResource.get(USER_ID)).thenReturn(userResource);
|
||||
when(userResource.getUserSessions()).thenReturn(Collections.emptyList());
|
||||
|
||||
int count = userService.logoutAllSessions(USER_ID, REALM);
|
||||
|
||||
verify(userResource).logout();
|
||||
assertEquals(0, count);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetActiveSessions() {
|
||||
when(keycloakAdminClient.getUsers(REALM)).thenReturn(usersResource);
|
||||
when(usersResource.get(USER_ID)).thenReturn(userResource);
|
||||
|
||||
// Mock UserSessionRepresentation
|
||||
org.keycloak.representations.idm.UserSessionRepresentation session1 =
|
||||
mock(org.keycloak.representations.idm.UserSessionRepresentation.class);
|
||||
when(session1.getId()).thenReturn("session-1");
|
||||
org.keycloak.representations.idm.UserSessionRepresentation session2 =
|
||||
mock(org.keycloak.representations.idm.UserSessionRepresentation.class);
|
||||
when(session2.getId()).thenReturn("session-2");
|
||||
|
||||
when(userResource.getUserSessions()).thenReturn(List.of(session1, session2));
|
||||
|
||||
List<String> sessions = userService.getActiveSessions(USER_ID, REALM);
|
||||
|
||||
assertNotNull(sessions);
|
||||
assertEquals(2, sessions.size());
|
||||
assertTrue(sessions.contains("session-1"));
|
||||
assertTrue(sessions.contains("session-2"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetActiveSessions_Empty() {
|
||||
when(keycloakAdminClient.getUsers(REALM)).thenReturn(usersResource);
|
||||
when(usersResource.get(USER_ID)).thenReturn(userResource);
|
||||
when(userResource.getUserSessions()).thenReturn(Collections.emptyList());
|
||||
|
||||
List<String> sessions = userService.getActiveSessions(USER_ID, REALM);
|
||||
|
||||
assertNotNull(sessions);
|
||||
assertTrue(sessions.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetActiveSessions_Exception() {
|
||||
when(keycloakAdminClient.getUsers(REALM)).thenReturn(usersResource);
|
||||
when(usersResource.get(USER_ID)).thenReturn(userResource);
|
||||
when(userResource.getUserSessions()).thenThrow(new RuntimeException("Error"));
|
||||
|
||||
List<String> sessions = userService.getActiveSessions(USER_ID, REALM);
|
||||
|
||||
assertNotNull(sessions);
|
||||
assertTrue(sessions.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetAllUsers() {
|
||||
when(keycloakAdminClient.getUsers(REALM)).thenReturn(usersResource);
|
||||
|
||||
UserRepresentation user1 = new UserRepresentation();
|
||||
user1.setId("user-1");
|
||||
user1.setUsername("user1");
|
||||
user1.setEnabled(true); // Important: définir enabled pour éviter NullPointerException
|
||||
UserRepresentation user2 = new UserRepresentation();
|
||||
user2.setId("user-2");
|
||||
user2.setUsername("user2");
|
||||
user2.setEnabled(true); // Important: définir enabled pour éviter NullPointerException
|
||||
|
||||
when(usersResource.list(anyInt(), anyInt())).thenReturn(List.of(user1, user2));
|
||||
when(usersResource.count()).thenReturn(2);
|
||||
|
||||
var result = userService.getAllUsers(REALM, 0, 20);
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals(2, result.getUsers().size());
|
||||
assertEquals(2L, result.getTotalCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetUserById_NotFound() {
|
||||
when(keycloakAdminClient.getUsers(REALM)).thenReturn(usersResource);
|
||||
when(usersResource.get(USER_ID)).thenReturn(userResource);
|
||||
when(userResource.toRepresentation()).thenThrow(new jakarta.ws.rs.NotFoundException());
|
||||
|
||||
Optional<UserDTO> result = userService.getUserById(USER_ID, REALM);
|
||||
|
||||
assertFalse(result.isPresent());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetUserById_ExceptionWith404() {
|
||||
when(keycloakAdminClient.getUsers(REALM)).thenReturn(usersResource);
|
||||
when(usersResource.get(USER_ID)).thenReturn(userResource);
|
||||
RuntimeException exception = new RuntimeException("Server response is: 404");
|
||||
when(userResource.toRepresentation()).thenThrow(exception);
|
||||
|
||||
Optional<UserDTO> result = userService.getUserById(USER_ID, REALM);
|
||||
|
||||
assertFalse(result.isPresent());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetUserByUsername_Success() {
|
||||
when(keycloakAdminClient.getUsers(REALM)).thenReturn(usersResource);
|
||||
|
||||
UserRepresentation userRep = new UserRepresentation();
|
||||
userRep.setId(USER_ID);
|
||||
userRep.setUsername("testuser");
|
||||
userRep.setEnabled(true);
|
||||
when(usersResource.search("testuser", 0, 1, true)).thenReturn(List.of(userRep));
|
||||
|
||||
Optional<UserDTO> result = userService.getUserByUsername("testuser", REALM);
|
||||
|
||||
assertTrue(result.isPresent());
|
||||
assertEquals("testuser", result.get().getUsername());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetUserByUsername_NotFound() {
|
||||
when(keycloakAdminClient.getUsers(REALM)).thenReturn(usersResource);
|
||||
when(usersResource.search("nonexistent", 0, 1, true)).thenReturn(Collections.emptyList());
|
||||
|
||||
Optional<UserDTO> result = userService.getUserByUsername("nonexistent", REALM);
|
||||
|
||||
assertFalse(result.isPresent());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetUserByUsername_Exception() {
|
||||
when(keycloakAdminClient.getUsers(REALM)).thenReturn(usersResource);
|
||||
when(usersResource.search("testuser", 0, 1, true)).thenThrow(new RuntimeException("Connection error"));
|
||||
|
||||
assertThrows(RuntimeException.class, () ->
|
||||
userService.getUserByUsername("testuser", REALM));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetUserByEmail_Success() {
|
||||
when(keycloakAdminClient.getUsers(REALM)).thenReturn(usersResource);
|
||||
|
||||
UserRepresentation userRep = new UserRepresentation();
|
||||
userRep.setId(USER_ID);
|
||||
userRep.setEmail("test@example.com");
|
||||
userRep.setEnabled(true);
|
||||
when(usersResource.searchByEmail("test@example.com", true)).thenReturn(List.of(userRep));
|
||||
|
||||
Optional<UserDTO> result = userService.getUserByEmail("test@example.com", REALM);
|
||||
|
||||
assertTrue(result.isPresent());
|
||||
assertEquals("test@example.com", result.get().getEmail());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetUserByEmail_NotFound() {
|
||||
when(keycloakAdminClient.getUsers(REALM)).thenReturn(usersResource);
|
||||
when(usersResource.searchByEmail("nonexistent@example.com", true)).thenReturn(Collections.emptyList());
|
||||
|
||||
Optional<UserDTO> result = userService.getUserByEmail("nonexistent@example.com", REALM);
|
||||
|
||||
assertFalse(result.isPresent());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetUserByEmail_Exception() {
|
||||
when(keycloakAdminClient.getUsers(REALM)).thenReturn(usersResource);
|
||||
when(usersResource.searchByEmail("test@example.com", true)).thenThrow(new RuntimeException("Connection error"));
|
||||
|
||||
assertThrows(RuntimeException.class, () ->
|
||||
userService.getUserByEmail("test@example.com", REALM));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreateUser_UsernameExists() {
|
||||
when(keycloakAdminClient.getUsers(REALM)).thenReturn(usersResource);
|
||||
// usernameExists calls search which should return a non-empty list
|
||||
UserRepresentation existingUser = new UserRepresentation();
|
||||
existingUser.setUsername("existinguser");
|
||||
existingUser.setEnabled(true);
|
||||
when(usersResource.search("existinguser", 0, 1, true)).thenReturn(List.of(existingUser));
|
||||
|
||||
UserDTO userDTO = UserDTO.builder()
|
||||
.username("existinguser")
|
||||
.email("test@example.com")
|
||||
.build();
|
||||
|
||||
// createUser catches all exceptions and rethrows as RuntimeException
|
||||
RuntimeException exception = assertThrows(RuntimeException.class, () ->
|
||||
userService.createUser(userDTO, REALM));
|
||||
assertTrue(exception.getCause() instanceof IllegalArgumentException);
|
||||
assertTrue(exception.getCause().getMessage().contains("existe déjà"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreateUser_EmailExists() {
|
||||
when(keycloakAdminClient.getUsers(REALM)).thenReturn(usersResource);
|
||||
when(usersResource.search("newuser", 0, 1, true)).thenReturn(Collections.emptyList());
|
||||
// emailExists calls searchByEmail which should return a non-empty list
|
||||
UserRepresentation existingUser = new UserRepresentation();
|
||||
existingUser.setEmail("existing@example.com");
|
||||
existingUser.setEnabled(true);
|
||||
when(usersResource.searchByEmail("existing@example.com", true)).thenReturn(List.of(existingUser));
|
||||
|
||||
UserDTO userDTO = UserDTO.builder()
|
||||
.username("newuser")
|
||||
.email("existing@example.com")
|
||||
.build();
|
||||
|
||||
// createUser catches all exceptions and rethrows as RuntimeException
|
||||
RuntimeException exception = assertThrows(RuntimeException.class, () ->
|
||||
userService.createUser(userDTO, REALM));
|
||||
assertTrue(exception.getCause() instanceof IllegalArgumentException);
|
||||
assertTrue(exception.getCause().getMessage().contains("existe déjà"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreateUser_StatusNot201() {
|
||||
when(keycloakAdminClient.getUsers(REALM)).thenReturn(usersResource);
|
||||
when(usersResource.search("newuser", 0, 1, true)).thenReturn(Collections.emptyList());
|
||||
|
||||
UserDTO userDTO = UserDTO.builder()
|
||||
.username("newuser")
|
||||
.email("test@example.com")
|
||||
.build();
|
||||
|
||||
jakarta.ws.rs.core.Response response = mock(jakarta.ws.rs.core.Response.class);
|
||||
when(response.getStatus()).thenReturn(400);
|
||||
when(response.getStatusInfo()).thenReturn(jakarta.ws.rs.core.Response.Status.BAD_REQUEST);
|
||||
when(usersResource.create(any())).thenReturn(response);
|
||||
|
||||
assertThrows(RuntimeException.class, () ->
|
||||
userService.createUser(userDTO, REALM));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreateUser_WithTemporaryPassword() {
|
||||
when(keycloakAdminClient.getUsers(REALM)).thenReturn(usersResource);
|
||||
when(usersResource.search("newuser", 0, 1, true)).thenReturn(Collections.emptyList());
|
||||
|
||||
UserDTO userDTO = UserDTO.builder()
|
||||
.username("newuser")
|
||||
.email("test@example.com")
|
||||
.temporaryPassword("temp123")
|
||||
.temporaryPasswordFlag(true)
|
||||
.build();
|
||||
|
||||
jakarta.ws.rs.core.Response response = mock(jakarta.ws.rs.core.Response.class);
|
||||
when(response.getStatus()).thenReturn(201);
|
||||
java.net.URI location = java.net.URI.create("http://localhost/users/" + USER_ID);
|
||||
when(response.getLocation()).thenReturn(location);
|
||||
when(usersResource.create(any())).thenReturn(response);
|
||||
when(usersResource.get(USER_ID)).thenReturn(userResource);
|
||||
|
||||
UserRepresentation createdUser = new UserRepresentation();
|
||||
createdUser.setId(USER_ID);
|
||||
createdUser.setUsername("newuser");
|
||||
createdUser.setEnabled(true);
|
||||
when(userResource.toRepresentation()).thenReturn(createdUser);
|
||||
|
||||
UserDTO result = userService.createUser(userDTO, REALM);
|
||||
|
||||
assertNotNull(result);
|
||||
verify(userResource).resetPassword(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreateUser_Exception() {
|
||||
when(keycloakAdminClient.getUsers(REALM)).thenReturn(usersResource);
|
||||
when(usersResource.search("newuser", 0, 1, true)).thenThrow(new RuntimeException("Connection error"));
|
||||
|
||||
UserDTO userDTO = UserDTO.builder()
|
||||
.username("newuser")
|
||||
.email("test@example.com")
|
||||
.build();
|
||||
|
||||
assertThrows(RuntimeException.class, () ->
|
||||
userService.createUser(userDTO, REALM));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testUpdateUser_WithEmailAndPrenom() {
|
||||
when(keycloakAdminClient.getUsers(REALM)).thenReturn(usersResource);
|
||||
when(usersResource.get(USER_ID)).thenReturn(userResource);
|
||||
|
||||
UserRepresentation existingUser = new UserRepresentation();
|
||||
existingUser.setId(USER_ID);
|
||||
existingUser.setUsername("testuser");
|
||||
existingUser.setEnabled(true);
|
||||
when(userResource.toRepresentation()).thenReturn(existingUser);
|
||||
|
||||
UserDTO userDTO = UserDTO.builder()
|
||||
.id(USER_ID)
|
||||
.email("newemail@example.com")
|
||||
.prenom("John")
|
||||
.build();
|
||||
|
||||
UserDTO result = userService.updateUser(USER_ID, userDTO, REALM);
|
||||
|
||||
assertNotNull(result);
|
||||
verify(userResource).update(any(UserRepresentation.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testActivateUser_Success() {
|
||||
when(keycloakAdminClient.getUsers(REALM)).thenReturn(usersResource);
|
||||
when(usersResource.get(USER_ID)).thenReturn(userResource);
|
||||
|
||||
UserRepresentation user = new UserRepresentation();
|
||||
user.setId(USER_ID);
|
||||
user.setEnabled(false);
|
||||
when(userResource.toRepresentation()).thenReturn(user);
|
||||
|
||||
userService.activateUser(USER_ID, REALM);
|
||||
|
||||
verify(userResource).update(any(UserRepresentation.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testActivateUser_Exception() {
|
||||
when(keycloakAdminClient.getUsers(REALM)).thenReturn(usersResource);
|
||||
when(usersResource.get(USER_ID)).thenReturn(userResource);
|
||||
when(userResource.toRepresentation()).thenThrow(new RuntimeException("Connection error"));
|
||||
|
||||
assertThrows(RuntimeException.class, () ->
|
||||
userService.activateUser(USER_ID, REALM));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDeactivateUser_Success() {
|
||||
when(keycloakAdminClient.getUsers(REALM)).thenReturn(usersResource);
|
||||
when(usersResource.get(USER_ID)).thenReturn(userResource);
|
||||
|
||||
UserRepresentation user = new UserRepresentation();
|
||||
user.setId(USER_ID);
|
||||
user.setEnabled(true);
|
||||
when(userResource.toRepresentation()).thenReturn(user);
|
||||
|
||||
userService.deactivateUser(USER_ID, REALM, "Test reason");
|
||||
|
||||
verify(userResource).update(any(UserRepresentation.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDeactivateUser_Exception() {
|
||||
when(keycloakAdminClient.getUsers(REALM)).thenReturn(usersResource);
|
||||
when(usersResource.get(USER_ID)).thenReturn(userResource);
|
||||
when(userResource.toRepresentation()).thenThrow(new RuntimeException("Connection error"));
|
||||
|
||||
assertThrows(RuntimeException.class, () ->
|
||||
userService.deactivateUser(USER_ID, REALM, "Test reason"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuspendUser() {
|
||||
when(keycloakAdminClient.getUsers(REALM)).thenReturn(usersResource);
|
||||
when(usersResource.get(USER_ID)).thenReturn(userResource);
|
||||
|
||||
UserRepresentation user = new UserRepresentation();
|
||||
user.setId(USER_ID);
|
||||
user.setEnabled(true);
|
||||
when(userResource.toRepresentation()).thenReturn(user);
|
||||
|
||||
userService.suspendUser(USER_ID, REALM, "Suspension reason", 30);
|
||||
|
||||
verify(userResource).update(any(UserRepresentation.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testUnlockUser() {
|
||||
when(keycloakAdminClient.getUsers(REALM)).thenReturn(usersResource);
|
||||
when(usersResource.get(USER_ID)).thenReturn(userResource);
|
||||
|
||||
UserRepresentation user = new UserRepresentation();
|
||||
user.setId(USER_ID);
|
||||
user.setEnabled(false);
|
||||
when(userResource.toRepresentation()).thenReturn(user);
|
||||
|
||||
userService.unlockUser(USER_ID, REALM);
|
||||
|
||||
verify(userResource).update(any(UserRepresentation.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLogoutAllSessions_WithSessions() {
|
||||
when(keycloakAdminClient.getUsers(REALM)).thenReturn(usersResource);
|
||||
when(usersResource.get(USER_ID)).thenReturn(userResource);
|
||||
|
||||
org.keycloak.representations.idm.UserSessionRepresentation session1 =
|
||||
mock(org.keycloak.representations.idm.UserSessionRepresentation.class);
|
||||
org.keycloak.representations.idm.UserSessionRepresentation session2 =
|
||||
mock(org.keycloak.representations.idm.UserSessionRepresentation.class);
|
||||
|
||||
when(userResource.getUserSessions()).thenReturn(List.of(session1, session2));
|
||||
doNothing().when(userResource).logout();
|
||||
|
||||
int count = userService.logoutAllSessions(USER_ID, REALM);
|
||||
|
||||
assertEquals(2, count);
|
||||
verify(userResource).logout();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLogoutAllSessions_NoSessions() {
|
||||
when(keycloakAdminClient.getUsers(REALM)).thenReturn(usersResource);
|
||||
when(usersResource.get(USER_ID)).thenReturn(userResource);
|
||||
when(userResource.getUserSessions()).thenReturn(Collections.emptyList());
|
||||
doNothing().when(userResource).logout();
|
||||
|
||||
int count = userService.logoutAllSessions(USER_ID, REALM);
|
||||
|
||||
assertEquals(0, count);
|
||||
verify(userResource).logout();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLogoutAllSessions_Exception() {
|
||||
when(keycloakAdminClient.getUsers(REALM)).thenReturn(usersResource);
|
||||
when(usersResource.get(USER_ID)).thenReturn(userResource);
|
||||
when(userResource.getUserSessions()).thenThrow(new RuntimeException("Connection error"));
|
||||
|
||||
assertThrows(RuntimeException.class, () ->
|
||||
userService.logoutAllSessions(USER_ID, REALM));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetActiveSessions_Success() {
|
||||
when(keycloakAdminClient.getUsers(REALM)).thenReturn(usersResource);
|
||||
when(usersResource.get(USER_ID)).thenReturn(userResource);
|
||||
|
||||
org.keycloak.representations.idm.UserSessionRepresentation session1 =
|
||||
mock(org.keycloak.representations.idm.UserSessionRepresentation.class);
|
||||
org.keycloak.representations.idm.UserSessionRepresentation session2 =
|
||||
mock(org.keycloak.representations.idm.UserSessionRepresentation.class);
|
||||
when(session1.getId()).thenReturn("session-1");
|
||||
when(session2.getId()).thenReturn("session-2");
|
||||
|
||||
when(userResource.getUserSessions()).thenReturn(List.of(session1, session2));
|
||||
|
||||
List<String> sessions = userService.getActiveSessions(USER_ID, REALM);
|
||||
|
||||
assertNotNull(sessions);
|
||||
assertEquals(2, sessions.size());
|
||||
assertTrue(sessions.contains("session-1"));
|
||||
assertTrue(sessions.contains("session-2"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,569 @@
|
||||
package dev.lions.user.manager.service.impl;
|
||||
|
||||
import dev.lions.user.manager.client.KeycloakAdminClient;
|
||||
import dev.lions.user.manager.dto.user.UserDTO;
|
||||
import dev.lions.user.manager.dto.user.UserSearchCriteriaDTO;
|
||||
import dev.lions.user.manager.dto.user.UserSearchResultDTO;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.keycloak.admin.client.resource.UserResource;
|
||||
import org.keycloak.admin.client.resource.UsersResource;
|
||||
import org.keycloak.representations.idm.RoleRepresentation;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
* Tests d'intégration pour UserServiceImpl - Cas limites et branches conditionnelles complexes
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class UserServiceImplIntegrationTest {
|
||||
|
||||
@Mock
|
||||
private KeycloakAdminClient keycloakAdminClient;
|
||||
|
||||
@Mock
|
||||
private UsersResource usersResource;
|
||||
|
||||
@Mock
|
||||
private UserResource userResource;
|
||||
|
||||
@InjectMocks
|
||||
private UserServiceImpl userService;
|
||||
|
||||
private static final String REALM = "test-realm";
|
||||
private static final String USER_ID = "user-123";
|
||||
|
||||
// ==================== Tests de recherche - Cas limites ====================
|
||||
|
||||
@Test
|
||||
void testSearchUsers_WithSearchTerm() {
|
||||
when(keycloakAdminClient.getUsers(REALM)).thenReturn(usersResource);
|
||||
|
||||
UserRepresentation user = new UserRepresentation();
|
||||
user.setId("1");
|
||||
user.setUsername("testuser");
|
||||
user.setEnabled(true);
|
||||
user.setEmailVerified(true);
|
||||
|
||||
when(usersResource.search("test", 0, 20)).thenReturn(List.of(user));
|
||||
when(usersResource.count()).thenReturn(1);
|
||||
|
||||
UserSearchCriteriaDTO criteria = UserSearchCriteriaDTO.builder()
|
||||
.realmName(REALM)
|
||||
.searchTerm("test")
|
||||
.page(0)
|
||||
.pageSize(20)
|
||||
.build();
|
||||
|
||||
UserSearchResultDTO result = userService.searchUsers(criteria);
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals(1, result.getUsers().size());
|
||||
verify(usersResource).search("test", 0, 20);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSearchUsers_WithSearchTerm_Blank() {
|
||||
when(keycloakAdminClient.getUsers(REALM)).thenReturn(usersResource);
|
||||
|
||||
UserRepresentation user = new UserRepresentation();
|
||||
user.setId("1");
|
||||
user.setUsername("user1");
|
||||
user.setEnabled(true);
|
||||
|
||||
when(usersResource.list(0, 20)).thenReturn(List.of(user));
|
||||
when(usersResource.count()).thenReturn(1);
|
||||
|
||||
UserSearchCriteriaDTO criteria = UserSearchCriteriaDTO.builder()
|
||||
.realmName(REALM)
|
||||
.searchTerm(" ") // Blank string
|
||||
.page(0)
|
||||
.pageSize(20)
|
||||
.build();
|
||||
|
||||
UserSearchResultDTO result = userService.searchUsers(criteria);
|
||||
|
||||
assertNotNull(result);
|
||||
verify(usersResource).list(0, 20); // Should use list() when searchTerm is blank
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSearchUsers_WithUsername() {
|
||||
when(keycloakAdminClient.getUsers(REALM)).thenReturn(usersResource);
|
||||
|
||||
UserRepresentation user = new UserRepresentation();
|
||||
user.setId("1");
|
||||
user.setUsername("exactuser");
|
||||
user.setEnabled(true);
|
||||
|
||||
when(usersResource.search("exactuser", 0, 20, true)).thenReturn(List.of(user));
|
||||
when(usersResource.count()).thenReturn(1);
|
||||
|
||||
UserSearchCriteriaDTO criteria = UserSearchCriteriaDTO.builder()
|
||||
.realmName(REALM)
|
||||
.username("exactuser")
|
||||
.page(0)
|
||||
.pageSize(20)
|
||||
.build();
|
||||
|
||||
UserSearchResultDTO result = userService.searchUsers(criteria);
|
||||
|
||||
assertNotNull(result);
|
||||
verify(usersResource).search("exactuser", 0, 20, true);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSearchUsers_WithEmail() {
|
||||
when(keycloakAdminClient.getUsers(REALM)).thenReturn(usersResource);
|
||||
|
||||
UserRepresentation user = new UserRepresentation();
|
||||
user.setId("1");
|
||||
user.setEmail("test@example.com");
|
||||
user.setEnabled(true);
|
||||
|
||||
when(usersResource.searchByEmail("test@example.com", true)).thenReturn(List.of(user));
|
||||
when(usersResource.count()).thenReturn(1);
|
||||
|
||||
UserSearchCriteriaDTO criteria = UserSearchCriteriaDTO.builder()
|
||||
.realmName(REALM)
|
||||
.email("test@example.com")
|
||||
.page(0)
|
||||
.pageSize(20)
|
||||
.build();
|
||||
|
||||
UserSearchResultDTO result = userService.searchUsers(criteria);
|
||||
|
||||
assertNotNull(result);
|
||||
verify(usersResource).searchByEmail("test@example.com", true);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSearchUsers_NoCriteria() {
|
||||
when(keycloakAdminClient.getUsers(REALM)).thenReturn(usersResource);
|
||||
|
||||
UserRepresentation user = new UserRepresentation();
|
||||
user.setId("1");
|
||||
user.setUsername("user1");
|
||||
user.setEnabled(true);
|
||||
|
||||
when(usersResource.list(0, 20)).thenReturn(List.of(user));
|
||||
when(usersResource.count()).thenReturn(1);
|
||||
|
||||
UserSearchCriteriaDTO criteria = UserSearchCriteriaDTO.builder()
|
||||
.realmName(REALM)
|
||||
.page(0)
|
||||
.pageSize(20)
|
||||
.build();
|
||||
|
||||
UserSearchResultDTO result = userService.searchUsers(criteria);
|
||||
|
||||
assertNotNull(result);
|
||||
verify(usersResource).list(0, 20);
|
||||
}
|
||||
|
||||
// ==================== Tests de filtrage - Cas limites ====================
|
||||
|
||||
@Test
|
||||
void testSearchUsers_FilterByEnabled_True() {
|
||||
when(keycloakAdminClient.getUsers(REALM)).thenReturn(usersResource);
|
||||
|
||||
UserRepresentation enabledUser = new UserRepresentation();
|
||||
enabledUser.setId("1");
|
||||
enabledUser.setUsername("enabled");
|
||||
enabledUser.setEnabled(true);
|
||||
|
||||
UserRepresentation disabledUser = new UserRepresentation();
|
||||
disabledUser.setId("2");
|
||||
disabledUser.setUsername("disabled");
|
||||
disabledUser.setEnabled(false);
|
||||
|
||||
when(usersResource.list(0, 20)).thenReturn(List.of(enabledUser, disabledUser));
|
||||
when(usersResource.count()).thenReturn(2);
|
||||
|
||||
UserSearchCriteriaDTO criteria = UserSearchCriteriaDTO.builder()
|
||||
.realmName(REALM)
|
||||
.enabled(true)
|
||||
.page(0)
|
||||
.pageSize(20)
|
||||
.build();
|
||||
|
||||
UserSearchResultDTO result = userService.searchUsers(criteria);
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals(1, result.getUsers().size());
|
||||
assertTrue(result.getUsers().get(0).getEnabled());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSearchUsers_FilterByEnabled_False() {
|
||||
when(keycloakAdminClient.getUsers(REALM)).thenReturn(usersResource);
|
||||
|
||||
UserRepresentation enabledUser = new UserRepresentation();
|
||||
enabledUser.setId("1");
|
||||
enabledUser.setUsername("enabled");
|
||||
enabledUser.setEnabled(true);
|
||||
|
||||
UserRepresentation disabledUser = new UserRepresentation();
|
||||
disabledUser.setId("2");
|
||||
disabledUser.setUsername("disabled");
|
||||
disabledUser.setEnabled(false);
|
||||
|
||||
when(usersResource.list(0, 20)).thenReturn(List.of(enabledUser, disabledUser));
|
||||
when(usersResource.count()).thenReturn(2);
|
||||
|
||||
UserSearchCriteriaDTO criteria = UserSearchCriteriaDTO.builder()
|
||||
.realmName(REALM)
|
||||
.enabled(false)
|
||||
.page(0)
|
||||
.pageSize(20)
|
||||
.build();
|
||||
|
||||
UserSearchResultDTO result = userService.searchUsers(criteria);
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals(1, result.getUsers().size());
|
||||
assertFalse(result.getUsers().get(0).getEnabled());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSearchUsers_FilterByEmailVerified_True() {
|
||||
when(keycloakAdminClient.getUsers(REALM)).thenReturn(usersResource);
|
||||
|
||||
UserRepresentation verifiedUser = new UserRepresentation();
|
||||
verifiedUser.setId("1");
|
||||
verifiedUser.setUsername("verified");
|
||||
verifiedUser.setEmail("test@example.com");
|
||||
verifiedUser.setEmailVerified(true);
|
||||
verifiedUser.setEnabled(true);
|
||||
|
||||
UserRepresentation unverifiedUser = new UserRepresentation();
|
||||
unverifiedUser.setId("2");
|
||||
unverifiedUser.setUsername("unverified");
|
||||
unverifiedUser.setEmail("test2@example.com");
|
||||
unverifiedUser.setEmailVerified(false);
|
||||
unverifiedUser.setEnabled(true);
|
||||
|
||||
when(usersResource.list(0, 20)).thenReturn(List.of(verifiedUser, unverifiedUser));
|
||||
when(usersResource.count()).thenReturn(2);
|
||||
|
||||
UserSearchCriteriaDTO criteria = UserSearchCriteriaDTO.builder()
|
||||
.realmName(REALM)
|
||||
.emailVerified(true)
|
||||
.page(0)
|
||||
.pageSize(20)
|
||||
.build();
|
||||
|
||||
UserSearchResultDTO result = userService.searchUsers(criteria);
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals(1, result.getUsers().size());
|
||||
assertTrue(result.getUsers().get(0).getEmailVerified());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSearchUsers_FilterByEnabledAndEmailVerified() {
|
||||
when(keycloakAdminClient.getUsers(REALM)).thenReturn(usersResource);
|
||||
|
||||
UserRepresentation user1 = new UserRepresentation();
|
||||
user1.setId("1");
|
||||
user1.setUsername("user1");
|
||||
user1.setEnabled(true);
|
||||
user1.setEmailVerified(true);
|
||||
|
||||
UserRepresentation user2 = new UserRepresentation();
|
||||
user2.setId("2");
|
||||
user2.setUsername("user2");
|
||||
user2.setEnabled(true);
|
||||
user2.setEmailVerified(false);
|
||||
|
||||
UserRepresentation user3 = new UserRepresentation();
|
||||
user3.setId("3");
|
||||
user3.setUsername("user3");
|
||||
user3.setEnabled(false);
|
||||
user3.setEmailVerified(true);
|
||||
|
||||
when(usersResource.list(0, 20)).thenReturn(List.of(user1, user2, user3));
|
||||
when(usersResource.count()).thenReturn(3);
|
||||
|
||||
UserSearchCriteriaDTO criteria = UserSearchCriteriaDTO.builder()
|
||||
.realmName(REALM)
|
||||
.enabled(true)
|
||||
.emailVerified(true)
|
||||
.page(0)
|
||||
.pageSize(20)
|
||||
.build();
|
||||
|
||||
UserSearchResultDTO result = userService.searchUsers(criteria);
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals(1, result.getUsers().size());
|
||||
assertEquals("user1", result.getUsers().get(0).getUsername());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSearchUsers_FilterByEnabled_Null() {
|
||||
when(keycloakAdminClient.getUsers(REALM)).thenReturn(usersResource);
|
||||
|
||||
UserRepresentation user1 = new UserRepresentation();
|
||||
user1.setId("1");
|
||||
user1.setUsername("user1");
|
||||
user1.setEnabled(true);
|
||||
|
||||
UserRepresentation user2 = new UserRepresentation();
|
||||
user2.setId("2");
|
||||
user2.setUsername("user2");
|
||||
user2.setEnabled(false);
|
||||
|
||||
when(usersResource.list(0, 20)).thenReturn(List.of(user1, user2));
|
||||
when(usersResource.count()).thenReturn(2);
|
||||
|
||||
UserSearchCriteriaDTO criteria = UserSearchCriteriaDTO.builder()
|
||||
.realmName(REALM)
|
||||
.enabled(null) // Null should not filter
|
||||
.page(0)
|
||||
.pageSize(20)
|
||||
.build();
|
||||
|
||||
UserSearchResultDTO result = userService.searchUsers(criteria);
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals(2, result.getUsers().size()); // Both users should be returned
|
||||
}
|
||||
|
||||
// ==================== Tests getUserById - Cas limites ====================
|
||||
|
||||
@Test
|
||||
void testGetUserById_WithRealmRoles() {
|
||||
when(keycloakAdminClient.getUsers(REALM)).thenReturn(usersResource);
|
||||
when(usersResource.get(USER_ID)).thenReturn(userResource);
|
||||
|
||||
UserRepresentation userRep = new UserRepresentation();
|
||||
userRep.setId(USER_ID);
|
||||
userRep.setUsername("testuser");
|
||||
userRep.setEnabled(true);
|
||||
when(userResource.toRepresentation()).thenReturn(userRep);
|
||||
|
||||
RoleRepresentation role1 = new RoleRepresentation();
|
||||
role1.setName("role1");
|
||||
RoleRepresentation role2 = new RoleRepresentation();
|
||||
role2.setName("role2");
|
||||
|
||||
when(userResource.roles()).thenReturn(mock(org.keycloak.admin.client.resource.RoleMappingResource.class));
|
||||
when(userResource.roles().realmLevel()).thenReturn(mock(org.keycloak.admin.client.resource.RoleScopeResource.class));
|
||||
when(userResource.roles().realmLevel().listAll()).thenReturn(List.of(role1, role2));
|
||||
|
||||
Optional<UserDTO> result = userService.getUserById(USER_ID, REALM);
|
||||
|
||||
assertTrue(result.isPresent());
|
||||
assertEquals(USER_ID, result.get().getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetUserById_WithEmptyRealmRoles() {
|
||||
when(keycloakAdminClient.getUsers(REALM)).thenReturn(usersResource);
|
||||
when(usersResource.get(USER_ID)).thenReturn(userResource);
|
||||
|
||||
UserRepresentation userRep = new UserRepresentation();
|
||||
userRep.setId(USER_ID);
|
||||
userRep.setUsername("testuser");
|
||||
userRep.setEnabled(true);
|
||||
when(userResource.toRepresentation()).thenReturn(userRep);
|
||||
|
||||
when(userResource.roles()).thenReturn(mock(org.keycloak.admin.client.resource.RoleMappingResource.class));
|
||||
when(userResource.roles().realmLevel()).thenReturn(mock(org.keycloak.admin.client.resource.RoleScopeResource.class));
|
||||
when(userResource.roles().realmLevel().listAll()).thenReturn(Collections.emptyList());
|
||||
|
||||
Optional<UserDTO> result = userService.getUserById(USER_ID, REALM);
|
||||
|
||||
assertTrue(result.isPresent());
|
||||
assertTrue(result.get().getRealmRoles() == null || result.get().getRealmRoles().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetUserById_WithNullRealmRoles() {
|
||||
when(keycloakAdminClient.getUsers(REALM)).thenReturn(usersResource);
|
||||
when(usersResource.get(USER_ID)).thenReturn(userResource);
|
||||
|
||||
UserRepresentation userRep = new UserRepresentation();
|
||||
userRep.setId(USER_ID);
|
||||
userRep.setUsername("testuser");
|
||||
userRep.setEnabled(true);
|
||||
when(userResource.toRepresentation()).thenReturn(userRep);
|
||||
|
||||
when(userResource.roles()).thenReturn(mock(org.keycloak.admin.client.resource.RoleMappingResource.class));
|
||||
when(userResource.roles().realmLevel()).thenReturn(mock(org.keycloak.admin.client.resource.RoleScopeResource.class));
|
||||
when(userResource.roles().realmLevel().listAll()).thenReturn(null);
|
||||
|
||||
Optional<UserDTO> result = userService.getUserById(USER_ID, REALM);
|
||||
|
||||
assertTrue(result.isPresent());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetUserById_WithExceptionInRolesRetrieval() {
|
||||
when(keycloakAdminClient.getUsers(REALM)).thenReturn(usersResource);
|
||||
when(usersResource.get(USER_ID)).thenReturn(userResource);
|
||||
|
||||
UserRepresentation userRep = new UserRepresentation();
|
||||
userRep.setId(USER_ID);
|
||||
userRep.setUsername("testuser");
|
||||
userRep.setEnabled(true);
|
||||
when(userResource.toRepresentation()).thenReturn(userRep);
|
||||
|
||||
when(userResource.roles()).thenReturn(mock(org.keycloak.admin.client.resource.RoleMappingResource.class));
|
||||
when(userResource.roles().realmLevel()).thenReturn(mock(org.keycloak.admin.client.resource.RoleScopeResource.class));
|
||||
when(userResource.roles().realmLevel().listAll()).thenThrow(new RuntimeException("Error getting roles"));
|
||||
|
||||
// Should not throw exception, just log warning
|
||||
Optional<UserDTO> result = userService.getUserById(USER_ID, REALM);
|
||||
|
||||
assertTrue(result.isPresent());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetUserById_With404InExceptionMessage() {
|
||||
when(keycloakAdminClient.getUsers(REALM)).thenReturn(usersResource);
|
||||
when(usersResource.get(USER_ID)).thenReturn(userResource);
|
||||
when(userResource.toRepresentation()).thenThrow(new RuntimeException("Server response is: 404"));
|
||||
|
||||
Optional<UserDTO> result = userService.getUserById(USER_ID, REALM);
|
||||
|
||||
assertFalse(result.isPresent());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetUserById_With404InExceptionMessage_Variant() {
|
||||
when(keycloakAdminClient.getUsers(REALM)).thenReturn(usersResource);
|
||||
when(usersResource.get(USER_ID)).thenReturn(userResource);
|
||||
when(userResource.toRepresentation()).thenThrow(new RuntimeException("Received: 'Server response is: 404'"));
|
||||
|
||||
Optional<UserDTO> result = userService.getUserById(USER_ID, REALM);
|
||||
|
||||
assertFalse(result.isPresent());
|
||||
}
|
||||
|
||||
// ==================== Tests usernameExists et emailExists ====================
|
||||
|
||||
@Test
|
||||
void testUsernameExists_True() {
|
||||
when(keycloakAdminClient.getUsers(REALM)).thenReturn(usersResource);
|
||||
|
||||
UserRepresentation user = new UserRepresentation();
|
||||
user.setUsername("existinguser");
|
||||
when(usersResource.search("existinguser", 0, 1, true)).thenReturn(List.of(user));
|
||||
|
||||
boolean exists = userService.usernameExists("existinguser", REALM);
|
||||
|
||||
assertTrue(exists);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testUsernameExists_False() {
|
||||
when(keycloakAdminClient.getUsers(REALM)).thenReturn(usersResource);
|
||||
when(usersResource.search("nonexistent", 0, 1, true)).thenReturn(Collections.emptyList());
|
||||
|
||||
boolean exists = userService.usernameExists("nonexistent", REALM);
|
||||
|
||||
assertFalse(exists);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testUsernameExists_Exception() {
|
||||
when(keycloakAdminClient.getUsers(REALM)).thenReturn(usersResource);
|
||||
when(usersResource.search("erroruser", 0, 1, true)).thenThrow(new RuntimeException("Error"));
|
||||
|
||||
boolean exists = userService.usernameExists("erroruser", REALM);
|
||||
|
||||
assertFalse(exists); // Should return false on exception
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEmailExists_True() {
|
||||
when(keycloakAdminClient.getUsers(REALM)).thenReturn(usersResource);
|
||||
|
||||
UserRepresentation user = new UserRepresentation();
|
||||
user.setEmail("existing@example.com");
|
||||
when(usersResource.searchByEmail("existing@example.com", true)).thenReturn(List.of(user));
|
||||
|
||||
boolean exists = userService.emailExists("existing@example.com", REALM);
|
||||
|
||||
assertTrue(exists);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEmailExists_False() {
|
||||
when(keycloakAdminClient.getUsers(REALM)).thenReturn(usersResource);
|
||||
when(usersResource.searchByEmail("nonexistent@example.com", true)).thenReturn(Collections.emptyList());
|
||||
|
||||
boolean exists = userService.emailExists("nonexistent@example.com", REALM);
|
||||
|
||||
assertFalse(exists);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEmailExists_Exception() {
|
||||
when(keycloakAdminClient.getUsers(REALM)).thenReturn(usersResource);
|
||||
when(usersResource.searchByEmail("error@example.com", true)).thenThrow(new RuntimeException("Error"));
|
||||
|
||||
boolean exists = userService.emailExists("error@example.com", REALM);
|
||||
|
||||
assertFalse(exists); // Should return false on exception
|
||||
}
|
||||
|
||||
// ==================== Tests countUsers ====================
|
||||
|
||||
@Test
|
||||
void testCountUsers_Success() {
|
||||
when(keycloakAdminClient.getUsers(REALM)).thenReturn(usersResource);
|
||||
when(usersResource.count()).thenReturn(42);
|
||||
|
||||
UserSearchCriteriaDTO criteria = UserSearchCriteriaDTO.builder()
|
||||
.realmName(REALM)
|
||||
.build();
|
||||
|
||||
long count = userService.countUsers(criteria);
|
||||
|
||||
assertEquals(42L, count);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCountUsers_Exception() {
|
||||
when(keycloakAdminClient.getUsers(REALM)).thenReturn(usersResource);
|
||||
when(usersResource.count()).thenThrow(new RuntimeException("Error"));
|
||||
|
||||
UserSearchCriteriaDTO criteria = UserSearchCriteriaDTO.builder()
|
||||
.realmName(REALM)
|
||||
.build();
|
||||
|
||||
long count = userService.countUsers(criteria);
|
||||
|
||||
assertEquals(0L, count); // Should return 0 on exception
|
||||
}
|
||||
|
||||
// ==================== Tests searchUsers - Exception handling ====================
|
||||
|
||||
@Test
|
||||
void testSearchUsers_Exception() {
|
||||
when(keycloakAdminClient.getUsers(REALM)).thenReturn(usersResource);
|
||||
when(usersResource.list(0, 20)).thenThrow(new RuntimeException("Connection error"));
|
||||
|
||||
UserSearchCriteriaDTO criteria = UserSearchCriteriaDTO.builder()
|
||||
.realmName(REALM)
|
||||
.page(0)
|
||||
.pageSize(20)
|
||||
.build();
|
||||
|
||||
assertThrows(RuntimeException.class, () -> userService.searchUsers(criteria));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
package dev.lions.user.manager.service.impl;
|
||||
|
||||
import dev.lions.user.manager.client.KeycloakAdminClient;
|
||||
import dev.lions.user.manager.dto.user.UserDTO;
|
||||
import dev.lions.user.manager.dto.user.UserSearchCriteriaDTO;
|
||||
import dev.lions.user.manager.dto.user.UserSearchResultDTO;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.keycloak.admin.client.resource.RoleMappingResource;
|
||||
import org.keycloak.admin.client.resource.RoleScopeResource;
|
||||
import org.keycloak.admin.client.resource.UserResource;
|
||||
import org.keycloak.admin.client.resource.UsersResource;
|
||||
import org.keycloak.representations.idm.RoleRepresentation;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class UserServiceImplTest {
|
||||
|
||||
@Mock
|
||||
KeycloakAdminClient keycloakAdminClient;
|
||||
|
||||
@Mock
|
||||
UsersResource usersResource;
|
||||
|
||||
@Mock
|
||||
UserResource userResource;
|
||||
|
||||
@Mock
|
||||
RoleMappingResource roleMappingResource;
|
||||
|
||||
@Mock
|
||||
RoleScopeResource roleScopeResource;
|
||||
|
||||
@InjectMocks
|
||||
UserServiceImpl userService;
|
||||
|
||||
private static final String REALM = "test-realm";
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
// lenient().when(keycloakAdminClient.getUsers(anyString())).thenReturn(usersResource);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSearchUsers() {
|
||||
when(keycloakAdminClient.getUsers(REALM)).thenReturn(usersResource);
|
||||
UserRepresentation userRep = new UserRepresentation();
|
||||
userRep.setId("1");
|
||||
userRep.setUsername("user");
|
||||
userRep.setEnabled(true);
|
||||
|
||||
when(usersResource.search(anyString(), anyInt(), anyInt())).thenReturn(Collections.singletonList(userRep));
|
||||
when(usersResource.count()).thenReturn(1);
|
||||
|
||||
UserSearchCriteriaDTO criteria = UserSearchCriteriaDTO.builder()
|
||||
.realmName(REALM)
|
||||
.searchTerm("user")
|
||||
.enabled(true)
|
||||
.build();
|
||||
|
||||
UserSearchResultDTO result = userService.searchUsers(criteria);
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals(1, result.getUsers().size());
|
||||
assertEquals("user", result.getUsers().get(0).getUsername());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetUserById() {
|
||||
when(keycloakAdminClient.getUsers(REALM)).thenReturn(usersResource);
|
||||
when(usersResource.get("1")).thenReturn(userResource);
|
||||
|
||||
UserRepresentation userRep = new UserRepresentation();
|
||||
userRep.setId("1");
|
||||
userRep.setUsername("user");
|
||||
userRep.setEnabled(true);
|
||||
when(userResource.toRepresentation()).thenReturn(userRep);
|
||||
|
||||
when(userResource.roles()).thenReturn(roleMappingResource);
|
||||
when(roleMappingResource.realmLevel()).thenReturn(roleScopeResource);
|
||||
when(roleScopeResource.listAll()).thenReturn(Collections.emptyList());
|
||||
|
||||
Optional<UserDTO> result = userService.getUserById("1", REALM);
|
||||
|
||||
assertTrue(result.isPresent());
|
||||
assertEquals("1", result.get().getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreateUser() {
|
||||
when(keycloakAdminClient.getUsers(REALM)).thenReturn(usersResource);
|
||||
UserDTO newUser = UserDTO.builder().username("newuser").email("new@example.com").build();
|
||||
|
||||
// Check exists
|
||||
when(usersResource.search("newuser", 0, 1, true)).thenReturn(Collections.emptyList());
|
||||
when(usersResource.searchByEmail("new@example.com", true)).thenReturn(Collections.emptyList());
|
||||
|
||||
// Mock creation response
|
||||
Response response = Response.status(201).location(URI.create("http://localhost/users/123")).build();
|
||||
when(usersResource.create(any(UserRepresentation.class))).thenReturn(response);
|
||||
|
||||
// Mock get created user
|
||||
when(usersResource.get("123")).thenReturn(userResource);
|
||||
UserRepresentation createdRep = new UserRepresentation();
|
||||
createdRep.setId("123");
|
||||
createdRep.setUsername("newuser");
|
||||
createdRep.setEnabled(true);
|
||||
when(userResource.toRepresentation()).thenReturn(createdRep);
|
||||
|
||||
UserDTO created = userService.createUser(newUser, REALM);
|
||||
|
||||
assertNotNull(created);
|
||||
assertEquals("123", created.getId());
|
||||
assertEquals("newuser", created.getUsername());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testUpdateUser() {
|
||||
when(keycloakAdminClient.getUsers(REALM)).thenReturn(usersResource);
|
||||
when(usersResource.get("1")).thenReturn(userResource);
|
||||
|
||||
UserRepresentation existing = new UserRepresentation();
|
||||
existing.setId("1");
|
||||
existing.setEnabled(true);
|
||||
when(userResource.toRepresentation()).thenReturn(existing);
|
||||
|
||||
UserDTO update = UserDTO.builder().username("updated").email("up@example.com").build();
|
||||
|
||||
UserDTO result = userService.updateUser("1", update, REALM);
|
||||
|
||||
verify(userResource).update(any(UserRepresentation.class));
|
||||
assertNotNull(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDeleteUser() {
|
||||
when(keycloakAdminClient.getUsers(REALM)).thenReturn(usersResource);
|
||||
when(usersResource.get("1")).thenReturn(userResource);
|
||||
|
||||
userService.deleteUser("1", REALM, true);
|
||||
verify(userResource).remove();
|
||||
|
||||
when(userResource.toRepresentation()).thenReturn(new UserRepresentation());
|
||||
userService.deleteUser("1", REALM, false);
|
||||
verify(userResource).update(any(UserRepresentation.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testActivateUser() {
|
||||
when(keycloakAdminClient.getUsers(REALM)).thenReturn(usersResource);
|
||||
when(usersResource.get("1")).thenReturn(userResource);
|
||||
when(userResource.toRepresentation()).thenReturn(new UserRepresentation());
|
||||
|
||||
userService.activateUser("1", REALM);
|
||||
|
||||
verify(userResource).update(argThat(rep -> rep.isEnabled()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testExportUsersToCSV() {
|
||||
when(keycloakAdminClient.getUsers(REALM)).thenReturn(usersResource);
|
||||
UserRepresentation user1 = new UserRepresentation();
|
||||
user1.setId("1");
|
||||
user1.setUsername("user1");
|
||||
user1.setEmail("user1@example.com");
|
||||
user1.setFirstName("First");
|
||||
user1.setLastName("Last");
|
||||
user1.setEnabled(true);
|
||||
|
||||
when(usersResource.list(any(), any())).thenReturn(Collections.singletonList(user1));
|
||||
when(usersResource.count()).thenReturn(1);
|
||||
|
||||
UserSearchCriteriaDTO criteria = UserSearchCriteriaDTO.builder()
|
||||
.realmName(REALM)
|
||||
.build();
|
||||
|
||||
String csv = userService.exportUsersToCSV(criteria);
|
||||
|
||||
assertNotNull(csv);
|
||||
assertTrue(csv.contains("username,email,firstName,lastName,enabled"));
|
||||
assertTrue(csv.contains("user1,user1@example.com,First,Last,true"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testImportUsersFromCSV() {
|
||||
when(keycloakAdminClient.getUsers(REALM)).thenReturn(usersResource);
|
||||
|
||||
// Mock checks for existing users
|
||||
lenient().when(usersResource.search(anyString(), anyInt(), anyInt(), anyBoolean()))
|
||||
.thenReturn(Collections.emptyList());
|
||||
lenient().when(usersResource.searchByEmail(anyString(), anyBoolean())).thenReturn(Collections.emptyList());
|
||||
|
||||
// Mock creation response
|
||||
Response response = Response.status(201).location(URI.create("http://localhost/users/123")).build();
|
||||
lenient().when(usersResource.create(any(UserRepresentation.class))).thenReturn(response);
|
||||
|
||||
// Mock retrieving created user
|
||||
lenient().when(usersResource.get(anyString())).thenReturn(userResource);
|
||||
UserRepresentation createdRep = new UserRepresentation();
|
||||
createdRep.setId("123");
|
||||
createdRep.setUsername("imported");
|
||||
createdRep.setEnabled(true);
|
||||
lenient().when(userResource.toRepresentation()).thenReturn(createdRep);
|
||||
|
||||
// For password setting
|
||||
lenient().doNothing().when(userResource)
|
||||
.resetPassword(any(org.keycloak.representations.idm.CredentialRepresentation.class));
|
||||
|
||||
String csvContent = "username,prenom,nom,email\n" +
|
||||
"imported,Imp,Orter,imp@test.com";
|
||||
|
||||
dev.lions.user.manager.dto.importexport.ImportResultDTO result =
|
||||
userService.importUsersFromCSV(csvContent, REALM);
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals(1, result.getSuccessCount());
|
||||
assertEquals(0, result.getErrorCount());
|
||||
verify(usersResource, atLeastOnce()).create(argThat(u -> u.getUsername().equals("imported")));
|
||||
}
|
||||
}
|
||||
27
src/test/resources/application-test.properties
Normal file
27
src/test/resources/application-test.properties
Normal file
@@ -0,0 +1,27 @@
|
||||
# Configuration pour les tests
|
||||
# Keycloak Admin Client Configuration (valeurs factices pour les tests)
|
||||
lions.keycloak.server-url=http://localhost:8080
|
||||
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=10
|
||||
lions.keycloak.timeout-seconds=30
|
||||
|
||||
# Quarkus-managed Keycloak Admin Client (tests)
|
||||
quarkus.keycloak.admin-client.server-url=${lions.keycloak.server-url}
|
||||
quarkus.keycloak.admin-client.realm=${lions.keycloak.admin-realm}
|
||||
quarkus.keycloak.admin-client.client-id=${lions.keycloak.admin-client-id}
|
||||
quarkus.keycloak.admin-client.username=${lions.keycloak.admin-username}
|
||||
quarkus.keycloak.admin-client.password=${lions.keycloak.admin-password}
|
||||
quarkus.keycloak.admin-client.grant-type=PASSWORD
|
||||
quarkus.keycloak.admin-client.enabled=false
|
||||
|
||||
# Keycloak OIDC Configuration (désactivé pour les tests)
|
||||
quarkus.oidc.tenant-enabled=false
|
||||
quarkus.keycloak.policy-enforcer.enable=false
|
||||
|
||||
# Logging pour les tests
|
||||
quarkus.log.level=WARN
|
||||
quarkus.log.category."dev.lions.user.manager".level=WARN
|
||||
|
||||
Reference in New Issue
Block a user