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:
dahoud
2026-04-12 15:04:23 +00:00
parent 2ed890803c
commit 8ab1513bf5
35 changed files with 5594 additions and 19 deletions

View File

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

View File

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

View File

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

View File

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

View File

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