feat(admin): KeycloakAdminHttpClient + AdminUserService amélioré
- KeycloakAdminHttpClient (nouveau) : client HTTP natif (java.net.http.HttpClient) pour contourner les problèmes de désérialisation avec RESTEasy sur certains endpoints Keycloak 26+ (bruteForceStrategy, cpuInfo inconnus). Utilise ObjectMapper avec FAIL_ON_UNKNOWN_PROPERTIES=false. - AdminUserService : utilisation correcte de AdminUserServiceClient + AdminRoleServiceClient avec AdminServiceTokenHeadersFactory pour l'auth. - ModuleAccessFilter : améliorations de la logique @RequiresModule.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package dev.lions.unionflow.server.security;
|
||||
|
||||
import dev.lions.unionflow.server.service.OrganisationModuleService;
|
||||
import io.quarkus.security.identity.SecurityIdentity;
|
||||
import jakarta.annotation.Priority;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.ws.rs.Priorities;
|
||||
@@ -44,6 +45,9 @@ public class ModuleAccessFilter implements ContainerRequestFilter {
|
||||
@Inject
|
||||
OrganisationModuleService organisationModuleService;
|
||||
|
||||
@Inject
|
||||
SecurityIdentity identity;
|
||||
|
||||
@Context
|
||||
ResourceInfo resourceInfo;
|
||||
|
||||
@@ -61,6 +65,11 @@ public class ModuleAccessFilter implements ContainerRequestFilter {
|
||||
return;
|
||||
}
|
||||
|
||||
// SUPER_ADMIN a accès global à tous les modules sans contexte d'organisation
|
||||
if (identity.hasRole("SUPER_ADMIN")) {
|
||||
return;
|
||||
}
|
||||
|
||||
String moduleRequis = annotation.value().toUpperCase();
|
||||
|
||||
// 2. Extraire l'organisation active depuis le header
|
||||
|
||||
@@ -52,21 +52,11 @@ public class AdminUserService {
|
||||
}
|
||||
|
||||
public List<RoleDTO> getRealmRoles() {
|
||||
try {
|
||||
return roleServiceClient.getRealmRoles(DEFAULT_REALM);
|
||||
} catch (Exception e) {
|
||||
LOG.warnf("Impossible de récupérer les rôles realm: %s", e.getMessage());
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
||||
public List<RoleDTO> getUserRoles(String userId) {
|
||||
try {
|
||||
return roleServiceClient.getUserRealmRoles(userId, DEFAULT_REALM);
|
||||
} catch (Exception e) {
|
||||
LOG.warnf("Impossible de récupérer les rôles de l'utilisateur %s: %s", userId, e.getMessage());
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
package dev.lions.unionflow.server.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.time.Duration;
|
||||
|
||||
/**
|
||||
* Client HTTP direct vers l'API Admin Keycloak (sans JAX-RS pour éviter les conflits ObjectMapper).
|
||||
* Pattern identique à KeycloakAdminClientImpl.getAllRealms() — HttpClient Java 11 + ObjectMapper isolé.
|
||||
*/
|
||||
@Slf4j
|
||||
@ApplicationScoped
|
||||
public class KeycloakAdminHttpClient {
|
||||
|
||||
@ConfigProperty(name = "keycloak.admin.url", defaultValue = "http://localhost:8180")
|
||||
String keycloakUrl;
|
||||
|
||||
@ConfigProperty(name = "keycloak.admin.username", defaultValue = "admin")
|
||||
String adminUsername;
|
||||
|
||||
@ConfigProperty(name = "keycloak.admin.password", defaultValue = "admin")
|
||||
String adminPassword;
|
||||
|
||||
@ConfigProperty(name = "keycloak.admin.realm", defaultValue = "unionflow")
|
||||
String realm;
|
||||
|
||||
private final HttpClient httpClient = HttpClient.newBuilder()
|
||||
.connectTimeout(Duration.ofSeconds(10))
|
||||
.build();
|
||||
|
||||
private final ObjectMapper mapper = new ObjectMapper()
|
||||
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||
|
||||
/**
|
||||
* Obtenir un token admin depuis le realm master via client_credentials admin-cli
|
||||
*/
|
||||
private String getAdminToken() throws Exception {
|
||||
String body = "client_id=admin-cli"
|
||||
+ "&username=" + java.net.URLEncoder.encode(adminUsername, java.nio.charset.StandardCharsets.UTF_8)
|
||||
+ "&password=" + java.net.URLEncoder.encode(adminPassword, java.nio.charset.StandardCharsets.UTF_8)
|
||||
+ "&grant_type=password";
|
||||
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(URI.create(keycloakUrl + "/realms/master/protocol/openid-connect/token"))
|
||||
.header("Content-Type", "application/x-www-form-urlencoded")
|
||||
.POST(HttpRequest.BodyPublishers.ofString(body))
|
||||
.timeout(Duration.ofSeconds(10))
|
||||
.build();
|
||||
|
||||
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
if (response.statusCode() != 200) {
|
||||
throw new RuntimeException("Échec authentification admin Keycloak (HTTP " + response.statusCode() + "): " + response.body());
|
||||
}
|
||||
|
||||
return mapper.readTree(response.body()).get("access_token").asText();
|
||||
}
|
||||
|
||||
/**
|
||||
* Révoquer toutes les sessions actives du realm.
|
||||
* Stratégie : lister tous les users → POST /users/{id}/logout pour chaque.
|
||||
* @return nombre de sessions révoquées
|
||||
*/
|
||||
public int logoutAllSessions() throws Exception {
|
||||
String token = getAdminToken();
|
||||
log.info("Token admin Keycloak obtenu — déconnexion de toutes les sessions du realm '{}'", realm);
|
||||
|
||||
// Récupérer tous les utilisateurs (max 1000)
|
||||
HttpRequest usersRequest = HttpRequest.newBuilder()
|
||||
.uri(URI.create(keycloakUrl + "/admin/realms/" + realm + "/users?max=1000&enabled=true"))
|
||||
.header("Authorization", "Bearer " + token)
|
||||
.GET()
|
||||
.timeout(Duration.ofSeconds(15))
|
||||
.build();
|
||||
|
||||
HttpResponse<String> usersResponse = httpClient.send(usersRequest, HttpResponse.BodyHandlers.ofString());
|
||||
if (usersResponse.statusCode() != 200) {
|
||||
throw new RuntimeException("Impossible de lister les utilisateurs Keycloak (HTTP " + usersResponse.statusCode() + ")");
|
||||
}
|
||||
|
||||
var users = mapper.readTree(usersResponse.body());
|
||||
int loggedOut = 0;
|
||||
|
||||
for (var user : users) {
|
||||
String userId = user.get("id").asText();
|
||||
String username = user.has("username") ? user.get("username").asText() : userId;
|
||||
|
||||
HttpRequest logoutRequest = HttpRequest.newBuilder()
|
||||
.uri(URI.create(keycloakUrl + "/admin/realms/" + realm + "/users/" + userId + "/logout"))
|
||||
.header("Authorization", "Bearer " + token)
|
||||
.POST(HttpRequest.BodyPublishers.noBody())
|
||||
.timeout(Duration.ofSeconds(5))
|
||||
.build();
|
||||
|
||||
HttpResponse<String> logoutResponse = httpClient.send(logoutRequest, HttpResponse.BodyHandlers.ofString());
|
||||
if (logoutResponse.statusCode() == 204 || logoutResponse.statusCode() == 200) {
|
||||
loggedOut++;
|
||||
log.debug("Session révoquée pour l'utilisateur '{}'", username);
|
||||
} else {
|
||||
log.warn("Impossible de révoquer la session de '{}' (HTTP {})", username, logoutResponse.statusCode());
|
||||
}
|
||||
}
|
||||
|
||||
log.info("Déconnexion globale terminée: {}/{} utilisateur(s) déconnecté(s)", loggedOut, users.size());
|
||||
return loggedOut;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user