feat(lum): KeycloakRealmSetupService + rôles RBAC UnionFlow + Jacoco 100%
- Ajoute KeycloakRealmSetupService : auto-initialisation des rôles realm (admin, user_manager, user_viewer, role_manager...) et assignation du rôle user_manager au service account unionflow-server au démarrage (idempotent, retries, thread séparé pour ne pas bloquer le démarrage) → Corrige le 403 sur resetPassword / changement de mot de passe premier login - UserResource : étend les @RolesAllowed avec ADMIN/SUPER_ADMIN/USER pour permettre aux appels inter-services unionflow-server d'accéder aux endpoints sans être bloqués par le RBAC LUM ; corrige sendVerificationEmail (retourne Response) - application-dev.properties : service-accounts.user-manager-clients=unionflow-server - application-prod.properties : client-id, credentials.secret, token.audience, auto-setup - application-test.properties : H2 in-memory (plus besoin de Docker pour les tests) - pom.xml : H2 scope test, Jacoco 100% enforcement (exclusions MapStruct/repos/setup), annotation processors MapStruct+Lombok explicites - .gitignore + .env ajouté (.env exclu du commit) - script/docker/.env.example : variables KEYCLOAK_ADMIN_USERNAME/PASSWORD documentées
This commit is contained in:
@@ -0,0 +1,397 @@
|
||||
package dev.lions.user.manager.config;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.quarkus.runtime.StartupEvent;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.enterprise.event.Observes;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URLEncoder;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Initialise les rôles nécessaires dans les realms Keycloak autorisés au démarrage,
|
||||
* et assigne le rôle {@code user_manager} aux service accounts des clients configurés.
|
||||
*
|
||||
* <p>Utilise l'API REST Admin Keycloak via {@code java.net.http.HttpClient} avec
|
||||
* {@code ObjectMapper(FAIL_ON_UNKNOWN_PROPERTIES=false)} pour éviter les erreurs de
|
||||
* désérialisation sur Keycloak 26+ (champs inconnus {@code bruteForceStrategy}, {@code cpuInfo}).
|
||||
*
|
||||
* <p>L'initialisation est <b>idempotente</b> : elle vérifie l'existence avant de créer
|
||||
* ou d'assigner. En cas d'erreur (Keycloak non disponible au démarrage), un simple
|
||||
* avertissement est loggué sans bloquer le démarrage de l'application.
|
||||
*
|
||||
* <h3>Configuration</h3>
|
||||
* <pre>
|
||||
* lions.keycloak.auto-setup.enabled=true
|
||||
* lions.keycloak.authorized-realms=unionflow,btpxpress
|
||||
* lions.keycloak.service-accounts.user-manager-clients=unionflow-server,btpxpress-server
|
||||
* </pre>
|
||||
*/
|
||||
@ApplicationScoped
|
||||
@Slf4j
|
||||
public class KeycloakRealmSetupService {
|
||||
|
||||
/** Rôles à créer dans chaque realm autorisé. */
|
||||
private static final List<String> REQUIRED_ROLES = List.of(
|
||||
"admin", "user_manager", "user_viewer",
|
||||
"role_manager", "role_viewer", "auditor", "sync_manager"
|
||||
);
|
||||
|
||||
@ConfigProperty(name = "lions.keycloak.server-url")
|
||||
String serverUrl;
|
||||
|
||||
@ConfigProperty(name = "lions.keycloak.authorized-realms", defaultValue = "unionflow")
|
||||
String authorizedRealms;
|
||||
|
||||
@ConfigProperty(name = "lions.keycloak.service-accounts.user-manager-clients")
|
||||
Optional<String> userManagerClients;
|
||||
|
||||
// Credentials admin Keycloak pour obtenir un token master (sans CDI RequestScoped)
|
||||
@ConfigProperty(name = "quarkus.keycloak.admin-client.server-url")
|
||||
String adminServerUrl;
|
||||
|
||||
@ConfigProperty(name = "quarkus.keycloak.admin-client.realm", defaultValue = "master")
|
||||
String adminRealm;
|
||||
|
||||
@ConfigProperty(name = "quarkus.keycloak.admin-client.client-id", defaultValue = "admin-cli")
|
||||
String adminClientId;
|
||||
|
||||
@ConfigProperty(name = "quarkus.keycloak.admin-client.username", defaultValue = "admin")
|
||||
String adminUsername;
|
||||
|
||||
@ConfigProperty(name = "quarkus.keycloak.admin-client.password", defaultValue = "admin")
|
||||
String adminPassword;
|
||||
|
||||
@ConfigProperty(name = "lions.keycloak.auto-setup.enabled", defaultValue = "true")
|
||||
boolean autoSetupEnabled;
|
||||
|
||||
/** Nombre de tentatives max si Keycloak n'est pas encore prêt au démarrage. */
|
||||
@ConfigProperty(name = "lions.keycloak.auto-setup.retry-max", defaultValue = "5")
|
||||
int retryMax;
|
||||
|
||||
/** Délai en secondes entre chaque tentative. */
|
||||
@ConfigProperty(name = "lions.keycloak.auto-setup.retry-delay-seconds", defaultValue = "5")
|
||||
int retryDelaySeconds;
|
||||
|
||||
void onStart(@Observes StartupEvent ev) {
|
||||
if (!autoSetupEnabled) {
|
||||
log.info("KeycloakRealmSetupService désactivé (lions.keycloak.auto-setup.enabled=false)");
|
||||
return;
|
||||
}
|
||||
|
||||
// Exécuter l'auto-setup dans un thread séparé pour ne pas bloquer le démarrage
|
||||
// et permettre les retries sans bloquer Quarkus
|
||||
Executors.newSingleThreadExecutor().execute(this::runSetupWithRetry);
|
||||
}
|
||||
|
||||
private void runSetupWithRetry() {
|
||||
for (int attempt = 1; attempt <= retryMax; attempt++) {
|
||||
try {
|
||||
log.info("Initialisation des rôles Keycloak (tentative {}/{})...", attempt, retryMax);
|
||||
HttpClient http = HttpClient.newHttpClient();
|
||||
String token = fetchAdminToken(http);
|
||||
ObjectMapper mapper = new ObjectMapper()
|
||||
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||
|
||||
for (String rawRealm : authorizedRealms.split(",")) {
|
||||
String realm = rawRealm.trim();
|
||||
if (realm.isBlank() || "master".equals(realm)) {
|
||||
continue;
|
||||
}
|
||||
log.info("Configuration du realm '{}'...", realm);
|
||||
setupRealmRoles(http, mapper, token, realm);
|
||||
assignUserManagerRoleToServiceAccounts(http, mapper, token, realm);
|
||||
}
|
||||
log.info("✅ Initialisation des rôles Keycloak terminée avec succès");
|
||||
return; // succès — on sort
|
||||
|
||||
} catch (Exception e) {
|
||||
if (attempt < retryMax) {
|
||||
log.warn("⚠️ Tentative {}/{} échouée ({}). Nouvelle tentative dans {}s...",
|
||||
attempt, retryMax, e.getMessage(), retryDelaySeconds);
|
||||
try {
|
||||
TimeUnit.SECONDS.sleep(retryDelaySeconds);
|
||||
} catch (InterruptedException ie) {
|
||||
Thread.currentThread().interrupt();
|
||||
log.warn("Auto-setup interrompu");
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
log.error("❌ Impossible d'initialiser les rôles Keycloak après {} tentatives. " +
|
||||
"Vérifier que Keycloak est accessible et que KEYCLOAK_ADMIN_PASSWORD est défini. " +
|
||||
"Le service account 'unionflow-server' n'aura pas le rôle 'user_manager' — " +
|
||||
"les changements de mot de passe premier login retourneront 403.",
|
||||
retryMax, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// Token admin Keycloak (appel HTTP direct, sans CDI RequestScoped)
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
private String fetchAdminToken(HttpClient http) throws Exception {
|
||||
String body = "grant_type=password"
|
||||
+ "&client_id=" + URLEncoder.encode(adminClientId, StandardCharsets.UTF_8)
|
||||
+ "&username=" + URLEncoder.encode(adminUsername, StandardCharsets.UTF_8)
|
||||
+ "&password=" + URLEncoder.encode(adminPassword, StandardCharsets.UTF_8);
|
||||
|
||||
HttpResponse<String> resp = http.send(
|
||||
HttpRequest.newBuilder()
|
||||
.uri(URI.create(adminServerUrl + "/realms/" + adminRealm
|
||||
+ "/protocol/openid-connect/token"))
|
||||
.header("Content-Type", "application/x-www-form-urlencoded")
|
||||
.POST(HttpRequest.BodyPublishers.ofString(body))
|
||||
.build(),
|
||||
HttpResponse.BodyHandlers.ofString()
|
||||
);
|
||||
|
||||
if (resp.statusCode() != 200) {
|
||||
throw new IllegalStateException("Impossible d'obtenir un token admin Keycloak (HTTP "
|
||||
+ resp.statusCode() + "): " + resp.body());
|
||||
}
|
||||
|
||||
ObjectMapper mapper = new ObjectMapper()
|
||||
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||
Map<String, Object> tokenResponse = mapper.readValue(resp.body(), new TypeReference<>() {});
|
||||
return (String) tokenResponse.get("access_token");
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// Création des rôles realm
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
private void setupRealmRoles(HttpClient http, ObjectMapper mapper, String token, String realm)
|
||||
throws Exception {
|
||||
|
||||
Set<String> existingNames = fetchExistingRoleNames(http, mapper, token, realm);
|
||||
if (existingNames == null) return; // realm inaccessible, déjà loggué
|
||||
|
||||
for (String roleName : REQUIRED_ROLES) {
|
||||
if (existingNames.contains(roleName)) {
|
||||
log.debug("Rôle '{}' déjà présent dans le realm '{}'", roleName, realm);
|
||||
continue;
|
||||
}
|
||||
createRole(http, token, realm, roleName);
|
||||
}
|
||||
}
|
||||
|
||||
private Set<String> fetchExistingRoleNames(HttpClient http, ObjectMapper mapper,
|
||||
String token, String realm) throws Exception {
|
||||
HttpResponse<String> resp = http.send(
|
||||
HttpRequest.newBuilder()
|
||||
.uri(URI.create(serverUrl + "/admin/realms/" + realm + "/roles"))
|
||||
.header("Authorization", "Bearer " + token)
|
||||
.header("Accept", "application/json")
|
||||
.GET().build(),
|
||||
HttpResponse.BodyHandlers.ofString()
|
||||
);
|
||||
|
||||
if (resp.statusCode() != 200) {
|
||||
log.warn("Impossible de lire les rôles du realm '{}' (HTTP {})", realm, resp.statusCode());
|
||||
return null;
|
||||
}
|
||||
|
||||
List<Map<String, Object>> roles = mapper.readValue(resp.body(), new TypeReference<>() {});
|
||||
Set<String> names = new HashSet<>();
|
||||
for (Map<String, Object> r : roles) {
|
||||
names.add((String) r.get("name"));
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
private void createRole(HttpClient http, String token, String realm, String roleName)
|
||||
throws Exception {
|
||||
String body = String.format(
|
||||
"{\"name\":\"%s\",\"description\":\"Rôle %s pour lions-user-manager\"}",
|
||||
roleName, roleName
|
||||
);
|
||||
HttpResponse<String> resp = http.send(
|
||||
HttpRequest.newBuilder()
|
||||
.uri(URI.create(serverUrl + "/admin/realms/" + realm + "/roles"))
|
||||
.header("Authorization", "Bearer " + token)
|
||||
.header("Content-Type", "application/json")
|
||||
.POST(HttpRequest.BodyPublishers.ofString(body)).build(),
|
||||
HttpResponse.BodyHandlers.ofString()
|
||||
);
|
||||
|
||||
if (resp.statusCode() == 201) {
|
||||
log.info("✅ Rôle '{}' créé dans le realm '{}'", roleName, realm);
|
||||
} else if (resp.statusCode() == 409) {
|
||||
log.debug("Rôle '{}' déjà existant dans le realm '{}' (race condition ignorée)", roleName, realm);
|
||||
} else {
|
||||
log.warn("Échec de création du rôle '{}' dans le realm '{}' (HTTP {}): {}",
|
||||
roleName, realm, resp.statusCode(), resp.body());
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// Assignation du rôle user_manager aux service accounts
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
private void assignUserManagerRoleToServiceAccounts(HttpClient http, ObjectMapper mapper,
|
||||
String token, String realm) throws Exception {
|
||||
if (userManagerClients.isEmpty() || userManagerClients.get().isBlank()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Map<String, Object> userManagerRole = fetchRoleByName(http, mapper, token, realm, "user_manager");
|
||||
if (userManagerRole == null) {
|
||||
log.warn("Rôle 'user_manager' introuvable dans le realm '{}', assignation ignorée", realm);
|
||||
return;
|
||||
}
|
||||
|
||||
for (String rawClient : userManagerClients.get().split(",")) {
|
||||
String clientId = rawClient.trim();
|
||||
if (clientId.isBlank()) continue;
|
||||
assignRoleToServiceAccount(http, mapper, token, realm, clientId, userManagerRole);
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, Object> fetchRoleByName(HttpClient http, ObjectMapper mapper,
|
||||
String token, String realm, String roleName)
|
||||
throws Exception {
|
||||
HttpResponse<String> resp = http.send(
|
||||
HttpRequest.newBuilder()
|
||||
.uri(URI.create(serverUrl + "/admin/realms/" + realm + "/roles/" + roleName))
|
||||
.header("Authorization", "Bearer " + token)
|
||||
.header("Accept", "application/json")
|
||||
.GET().build(),
|
||||
HttpResponse.BodyHandlers.ofString()
|
||||
);
|
||||
|
||||
if (resp.statusCode() != 200) {
|
||||
log.warn("Rôle '{}' introuvable dans le realm '{}' (HTTP {})", roleName, realm, resp.statusCode());
|
||||
return null;
|
||||
}
|
||||
return mapper.readValue(resp.body(), new TypeReference<>() {});
|
||||
}
|
||||
|
||||
private void assignRoleToServiceAccount(HttpClient http, ObjectMapper mapper, String token,
|
||||
String realm, String clientId,
|
||||
Map<String, Object> role) throws Exception {
|
||||
// 1. Trouver l'UUID interne du client
|
||||
String internalClientId = findClientInternalId(http, mapper, token, realm, clientId);
|
||||
if (internalClientId == null) return;
|
||||
|
||||
// 2. Récupérer l'utilisateur service account du client
|
||||
String serviceAccountUserId = findServiceAccountUserId(http, mapper, token, realm, clientId, internalClientId);
|
||||
if (serviceAccountUserId == null) return;
|
||||
|
||||
// 3. Vérifier si le rôle est déjà assigné
|
||||
if (isRoleAlreadyAssigned(http, mapper, token, realm, serviceAccountUserId, (String) role.get("name"))) {
|
||||
log.debug("Rôle '{}' déjà assigné au service account du client '{}' dans le realm '{}'",
|
||||
role.get("name"), clientId, realm);
|
||||
return;
|
||||
}
|
||||
|
||||
// 4. Assigner le rôle
|
||||
ObjectMapper cleanMapper = new ObjectMapper()
|
||||
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||
String body = "[" + cleanMapper.writeValueAsString(role) + "]";
|
||||
|
||||
HttpResponse<String> assignResp = http.send(
|
||||
HttpRequest.newBuilder()
|
||||
.uri(URI.create(serverUrl + "/admin/realms/" + realm
|
||||
+ "/users/" + serviceAccountUserId + "/role-mappings/realm"))
|
||||
.header("Authorization", "Bearer " + token)
|
||||
.header("Content-Type", "application/json")
|
||||
.POST(HttpRequest.BodyPublishers.ofString(body)).build(),
|
||||
HttpResponse.BodyHandlers.ofString()
|
||||
);
|
||||
|
||||
if (assignResp.statusCode() == 204) {
|
||||
log.info("✅ Rôle 'user_manager' assigné au service account du client '{}' dans le realm '{}'",
|
||||
clientId, realm);
|
||||
} else {
|
||||
log.warn("Échec d'assignation du rôle 'user_manager' au service account '{}' dans le realm '{}' (HTTP {}): {}",
|
||||
clientId, realm, assignResp.statusCode(), assignResp.body());
|
||||
}
|
||||
}
|
||||
|
||||
private String findClientInternalId(HttpClient http, ObjectMapper mapper, String token,
|
||||
String realm, String clientId) throws Exception {
|
||||
HttpResponse<String> resp = http.send(
|
||||
HttpRequest.newBuilder()
|
||||
.uri(URI.create(serverUrl + "/admin/realms/" + realm
|
||||
+ "/clients?clientId=" + clientId + "&search=false"))
|
||||
.header("Authorization", "Bearer " + token)
|
||||
.header("Accept", "application/json")
|
||||
.GET().build(),
|
||||
HttpResponse.BodyHandlers.ofString()
|
||||
);
|
||||
|
||||
if (resp.statusCode() != 200) {
|
||||
log.warn("Impossible de rechercher le client '{}' dans le realm '{}' (HTTP {})",
|
||||
clientId, realm, resp.statusCode());
|
||||
return null;
|
||||
}
|
||||
|
||||
List<Map<String, Object>> clients = mapper.readValue(resp.body(), new TypeReference<>() {});
|
||||
if (clients.isEmpty()) {
|
||||
log.info("Client '{}' absent du realm '{}' — pas d'assignation service account dans ce realm " +
|
||||
"(normal si le client ne s'authentifie pas via ce realm)", clientId, realm);
|
||||
return null;
|
||||
}
|
||||
return (String) clients.get(0).get("id");
|
||||
}
|
||||
|
||||
private String findServiceAccountUserId(HttpClient http, ObjectMapper mapper, String token,
|
||||
String realm, String clientId, String internalClientId)
|
||||
throws Exception {
|
||||
HttpResponse<String> resp = http.send(
|
||||
HttpRequest.newBuilder()
|
||||
.uri(URI.create(serverUrl + "/admin/realms/" + realm
|
||||
+ "/clients/" + internalClientId + "/service-account-user"))
|
||||
.header("Authorization", "Bearer " + token)
|
||||
.header("Accept", "application/json")
|
||||
.GET().build(),
|
||||
HttpResponse.BodyHandlers.ofString()
|
||||
);
|
||||
|
||||
if (resp.statusCode() != 200) {
|
||||
log.warn("Service account introuvable pour le client '{}' dans le realm '{}' " +
|
||||
"(HTTP {} — le client est-il confidentiel avec serviceAccountsEnabled=true ?)",
|
||||
clientId, realm, resp.statusCode());
|
||||
return null;
|
||||
}
|
||||
|
||||
Map<String, Object> saUser = mapper.readValue(resp.body(), new TypeReference<>() {});
|
||||
return (String) saUser.get("id");
|
||||
}
|
||||
|
||||
private boolean isRoleAlreadyAssigned(HttpClient http, ObjectMapper mapper, String token,
|
||||
String realm, String userId, String roleName)
|
||||
throws Exception {
|
||||
HttpResponse<String> resp = http.send(
|
||||
HttpRequest.newBuilder()
|
||||
.uri(URI.create(serverUrl + "/admin/realms/" + realm
|
||||
+ "/users/" + userId + "/role-mappings/realm"))
|
||||
.header("Authorization", "Bearer " + token)
|
||||
.header("Accept", "application/json")
|
||||
.GET().build(),
|
||||
HttpResponse.BodyHandlers.ofString()
|
||||
);
|
||||
|
||||
if (resp.statusCode() != 200) return false;
|
||||
|
||||
List<Map<String, Object>> assigned = mapper.readValue(resp.body(), new TypeReference<>() {});
|
||||
return assigned.stream().anyMatch(r -> roleName.equals(r.get("name")));
|
||||
}
|
||||
}
|
||||
@@ -32,14 +32,14 @@ public class UserResource implements UserResourceApi {
|
||||
UserService userService;
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "user_manager" })
|
||||
@RolesAllowed({ "admin", "user_manager", "ADMIN", "SUPER_ADMIN" })
|
||||
public UserSearchResultDTO searchUsers(@Valid @NotNull UserSearchCriteriaDTO criteria) {
|
||||
log.info("POST /api/users/search - Recherche d'utilisateurs");
|
||||
return userService.searchUsers(criteria);
|
||||
}
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "user_manager", "user_viewer" })
|
||||
@RolesAllowed({ "admin", "user_manager", "user_viewer", "ADMIN", "SUPER_ADMIN", "USER" })
|
||||
public UserDTO getUserById(String userId, String realmName) {
|
||||
log.info("GET /api/users/{} - realm: {}", userId, realmName);
|
||||
return userService.getUserById(userId, realmName)
|
||||
@@ -48,14 +48,14 @@ public class UserResource implements UserResourceApi {
|
||||
}
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "user_manager", "user_viewer" })
|
||||
@RolesAllowed({ "admin", "user_manager", "user_viewer", "ADMIN", "SUPER_ADMIN", "USER" })
|
||||
public UserSearchResultDTO getAllUsers(String realmName, int page, int pageSize) {
|
||||
log.info("GET /api/users - realm: {}, page: {}, pageSize: {}", realmName, page, pageSize);
|
||||
return userService.getAllUsers(realmName, page, pageSize);
|
||||
}
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "user_manager" })
|
||||
@RolesAllowed({ "admin", "user_manager", "ADMIN", "SUPER_ADMIN" })
|
||||
public Response createUser(@Valid @NotNull UserDTO user, String realmName) {
|
||||
log.info("POST /api/users - Création d'un utilisateur: {}", user.getUsername());
|
||||
|
||||
@@ -74,28 +74,28 @@ public class UserResource implements UserResourceApi {
|
||||
}
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "user_manager" })
|
||||
@RolesAllowed({ "admin", "user_manager", "ADMIN", "SUPER_ADMIN" })
|
||||
public UserDTO updateUser(String userId, @Valid @NotNull UserDTO user, String realmName) {
|
||||
log.info("PUT /api/users/{} - Mise à jour", userId);
|
||||
return userService.updateUser(userId, user, realmName);
|
||||
}
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin" })
|
||||
@RolesAllowed({ "admin", "ADMIN", "SUPER_ADMIN" })
|
||||
public void deleteUser(String userId, String realmName, boolean hardDelete) {
|
||||
log.info("DELETE /api/users/{} - realm: {}, hardDelete: {}", userId, realmName, hardDelete);
|
||||
userService.deleteUser(userId, realmName, hardDelete);
|
||||
}
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "user_manager" })
|
||||
@RolesAllowed({ "admin", "user_manager", "ADMIN", "SUPER_ADMIN" })
|
||||
public void activateUser(String userId, String realmName) {
|
||||
log.info("POST /api/users/{}/activate", userId);
|
||||
userService.activateUser(userId, realmName);
|
||||
}
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "user_manager" })
|
||||
@RolesAllowed({ "admin", "user_manager", "ADMIN", "SUPER_ADMIN" })
|
||||
public void deactivateUser(String userId, String realmName, String raison) {
|
||||
log.info("POST /api/users/{}/deactivate - raison: {}", userId, raison);
|
||||
userService.deactivateUser(userId, realmName, raison);
|
||||
@@ -110,9 +110,10 @@ public class UserResource implements UserResourceApi {
|
||||
|
||||
@Override
|
||||
@RolesAllowed({ "admin", "user_manager" })
|
||||
public void sendVerificationEmail(String userId, String realmName) {
|
||||
public Response sendVerificationEmail(String userId, String realmName) {
|
||||
log.info("POST /api/users/{}/send-verification-email", userId);
|
||||
userService.sendVerificationEmail(userId, realmName);
|
||||
return Response.accepted().build();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -16,27 +16,39 @@ quarkus.http.cors.origins=http://localhost:3000,http://localhost:8080,http://loc
|
||||
# OIDC Configuration DEV
|
||||
# ============================================
|
||||
quarkus.oidc.enabled=true
|
||||
# realm lions-user-manager : cohérent avec le client web ET les appels inter-services
|
||||
# (unionflow-server doit aussi utiliser lions-user-manager realm pour appeler LUM)
|
||||
quarkus.oidc.auth-server-url=http://localhost:8180/realms/lions-user-manager
|
||||
quarkus.oidc.client-id=lions-user-manager-server
|
||||
quarkus.oidc.credentials.secret=${KEYCLOAK_CLIENT_SECRET:V3nP8kRzW5yX2mTqBcE7aJdFuHsL4gYo}
|
||||
quarkus.oidc.token.issuer=http://localhost:8180/realms/lions-user-manager
|
||||
# Audience : les tokens doivent contenir lions-user-manager-server dans le claim aud
|
||||
quarkus.oidc.token.audience=lions-user-manager-server
|
||||
quarkus.oidc.tls.verification=none
|
||||
|
||||
# ============================================
|
||||
# Keycloak Admin Client Configuration DEV
|
||||
# ============================================
|
||||
lions.keycloak.server-url=http://localhost:8180
|
||||
lions.keycloak.admin-username=admin
|
||||
lions.keycloak.admin-password=admin
|
||||
lions.keycloak.admin-username=${KEYCLOAK_ADMIN_USERNAME:admin}
|
||||
lions.keycloak.admin-password=${KEYCLOAK_ADMIN_PASSWORD:admin}
|
||||
lions.keycloak.connection-pool-size=5
|
||||
lions.keycloak.timeout-seconds=30
|
||||
lions.keycloak.authorized-realms=lions-user-manager,master,btpxpress,test-realm
|
||||
# Realms autorisés — uniquement ceux qui existent localement
|
||||
# master est exclu par le code (skip explicite), btpxpress/test-realm n'existent pas en dev
|
||||
lions.keycloak.authorized-realms=unionflow,lions-user-manager
|
||||
|
||||
# Clients dont le service account doit recevoir le rôle user_manager au démarrage
|
||||
lions.keycloak.service-accounts.user-manager-clients=unionflow-server
|
||||
|
||||
# Quarkus-managed Keycloak Admin Client DEV
|
||||
quarkus.keycloak.admin-client.server-url=http://localhost:8180
|
||||
quarkus.keycloak.admin-client.realm=master
|
||||
quarkus.keycloak.admin-client.client-id=admin-cli
|
||||
quarkus.keycloak.admin-client.grant-type=PASSWORD
|
||||
quarkus.keycloak.admin-client.username=admin
|
||||
quarkus.keycloak.admin-client.password=admin
|
||||
quarkus.keycloak.admin-client.username=${KEYCLOAK_ADMIN_USERNAME:admin}
|
||||
# Valeur par défaut "admin" pour l'environnement de développement local
|
||||
quarkus.keycloak.admin-client.password=${KEYCLOAK_ADMIN_PASSWORD:admin}
|
||||
|
||||
# ============================================
|
||||
# Audit Configuration DEV
|
||||
|
||||
@@ -23,7 +23,11 @@ quarkus.http.proxy.enable-forwarded-prefix=true
|
||||
# ============================================
|
||||
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}
|
||||
quarkus.oidc.credentials.secret=${KEYCLOAK_CLIENT_SECRET:kUIgIVf65f5NfRLRfbtG8jDhMvMpL0m0}
|
||||
quarkus.oidc.token.issuer=${KEYCLOAK_AUTH_SERVER_URL:https://security.lions.dev/realms/lions-user-manager}
|
||||
# Audience : les tokens doivent contenir lions-user-manager-server dans le claim aud
|
||||
quarkus.oidc.token.audience=lions-user-manager-server
|
||||
quarkus.oidc.tls.verification=required
|
||||
|
||||
# ============================================
|
||||
@@ -35,6 +39,9 @@ lions.keycloak.admin-password=${KEYCLOAK_ADMIN_PASSWORD:KeycloakAdmin2025!}
|
||||
lions.keycloak.connection-pool-size=20
|
||||
lions.keycloak.timeout-seconds=60
|
||||
lions.keycloak.authorized-realms=${KEYCLOAK_AUTHORIZED_REALMS:lions-user-manager,btpxpress,master,unionflow}
|
||||
# En prod, l'auto-setup est désactivé par défaut (géré via LIONS_KEYCLOAK_AUTO_SETUP=true si désiré)
|
||||
lions.keycloak.auto-setup.enabled=${LIONS_KEYCLOAK_AUTO_SETUP:false}
|
||||
lions.keycloak.service-accounts.user-manager-clients=${LIONS_SERVICE_ACCOUNTS_USER_MANAGER:}
|
||||
|
||||
# Quarkus-managed Keycloak Admin Client PROD
|
||||
quarkus.keycloak.admin-client.server-url=${KEYCLOAK_SERVER_URL:https://security.lions.dev}
|
||||
|
||||
@@ -25,13 +25,21 @@ quarkus.http.cors.headers=*
|
||||
quarkus.oidc.application-type=service
|
||||
quarkus.oidc.discovery-enabled=true
|
||||
quarkus.oidc.roles.role-claim-path=realm_access/roles
|
||||
quarkus.oidc.token.audience=account
|
||||
# Pas de vérification d'audience stricte (surchargé par application-dev.properties)
|
||||
# quarkus.oidc.token.audience=account
|
||||
|
||||
# ============================================
|
||||
# Keycloak Admin Client (COMMUNE)
|
||||
# ============================================
|
||||
lions.keycloak.admin-realm=master
|
||||
lions.keycloak.admin-client-id=admin-cli
|
||||
# Auto-setup des rôles au démarrage (désactiver en prod via env var LIONS_KEYCLOAK_AUTO_SETUP=false)
|
||||
lions.keycloak.auto-setup.enabled=${LIONS_KEYCLOAK_AUTO_SETUP:true}
|
||||
# Retry si Keycloak n'est pas prêt au démarrage (race condition docker/k8s)
|
||||
lions.keycloak.auto-setup.retry-max=${LIONS_KEYCLOAK_SETUP_RETRY_MAX:5}
|
||||
lions.keycloak.auto-setup.retry-delay-seconds=${LIONS_KEYCLOAK_SETUP_RETRY_DELAY:5}
|
||||
# Clients dont le service account doit recevoir le rôle user_manager (surchargé par profil)
|
||||
lions.keycloak.service-accounts.user-manager-clients=${LIONS_SERVICE_ACCOUNTS_USER_MANAGER:}
|
||||
|
||||
# Quarkus-managed Keycloak Admin Client (uses Quarkus ObjectMapper with fail-on-unknown-properties=false)
|
||||
quarkus.keycloak.admin-client.realm=${lions.keycloak.admin-realm}
|
||||
|
||||
Reference in New Issue
Block a user