Files
btpxpress-backend/src/main/java/dev/lions/btpxpress/adapter/http/PermissionResource.java
DahoudG 7df5f346f1 Refactor: Backend Frontend-Centric Auth - Suppression OIDC, validation JWT
Architecture modifiée pour Frontend-Centric Authentication:

1. **Suppression des dépendances OIDC**
   - quarkus-oidc → quarkus-smallrye-jwt
   - quarkus-keycloak-authorization → quarkus-smallrye-jwt-build
   - Le backend ne gère plus l'authentification OAuth

2. **Configuration JWT simple**
   - Validation des tokens JWT envoyés par le frontend
   - mp.jwt.verify.publickey.location (JWKS de Keycloak)
   - mp.jwt.verify.issuer (Keycloak realm)
   - Authentification via Authorization: Bearer header

3. **Suppression configurations OIDC**
   - application.properties: Suppression %dev.quarkus.oidc.*
   - application.properties: Suppression %prod.quarkus.oidc.*
   - application-prod.properties: Remplacement par mp.jwt.*
   - Logging: io.quarkus.oidc → io.quarkus.smallrye.jwt

4. **Sécurité simplifiée**
   - quarkus.security.auth.proactive=false
   - @Authenticated sur les endpoints
   - CORS configuré pour le frontend
   - Endpoints publics: /q/*, /openapi, /swagger-ui/*

Flux d'authentification:
1️⃣ Frontend → Keycloak (OAuth login)
2️⃣ Frontend ← Keycloak (access_token)
3️⃣ Frontend → Backend (Authorization: Bearer token)
4️⃣ Backend valide le token JWT (signature + issuer)
5️⃣ Backend → Frontend (données API)

Avantages:
 Pas de secret backend à gérer
 Pas de client btpxpress-backend dans Keycloak
 Séparation claire frontend/backend
 Backend devient une API REST stateless
 Tokens gérés par le frontend (localStorage/sessionStorage)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-31 17:05:11 +00:00

355 lines
14 KiB
Java

package dev.lions.btpxpress.adapter.http;
import dev.lions.btpxpress.application.service.PermissionService;
import dev.lions.btpxpress.domain.core.entity.Permission;
import dev.lions.btpxpress.domain.core.entity.Permission.PermissionCategory;
import dev.lions.btpxpress.domain.core.entity.UserRole;
import jakarta.inject.Inject;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* API REST pour la gestion des permissions EXPOSITION: Consultation des droits d'accès et
* permissions par rôle
*/
@Path("/api/permissions")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class PermissionResource {
private static final Logger logger = LoggerFactory.getLogger(PermissionResource.class);
@Inject PermissionService permissionService;
// === ENDPOINTS DE CONSULTATION ===
/** Récupère toutes les permissions disponibles */
@GET
@Path("/all")
public Response getAllPermissions() {
try {
logger.debug("GET /api/permissions/all");
List<Object> permissions =
Arrays.stream(Permission.values())
.map(
p ->
Map.of(
"code", p.getCode(),
"description", p.getDescription(),
"category", p.getCategory().name(),
"categoryDisplay", p.getCategory().getDisplayName()))
.collect(Collectors.toList());
return Response.ok(permissions).build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération des permissions", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération des permissions: " + e.getMessage())
.build();
}
}
/** Récupère les permissions par catégorie */
@GET
@Path("/categories")
public Response getPermissionsByCategory() {
try {
logger.debug("GET /api/permissions/categories");
Map<String, Object> result =
Arrays.stream(PermissionCategory.values())
.collect(
Collectors.toMap(
category -> category.name(),
category ->
Map.of(
"displayName", category.getDisplayName(),
"permissions",
Permission.getByCategory(category).stream()
.map(
p ->
Map.of(
"code", p.getCode(),
"description", p.getDescription()))
.collect(Collectors.toList()))));
return Response.ok(result).build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération des permissions par catégorie", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération des permissions: " + e.getMessage())
.build();
}
}
/** Récupère les permissions d'un rôle spécifique */
@GET
@Path("/role/{role}")
public Response getPermissionsByRole(@PathParam("role") String roleStr) {
try {
logger.debug("GET /api/permissions/role/{}", roleStr);
UserRole role = UserRole.valueOf(roleStr.toUpperCase());
Map<String, Object> summary = permissionService.getPermissionSummary(role);
return Response.ok(summary).build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("Rôle invalide: " + roleStr)
.build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération des permissions pour le rôle: " + roleStr, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération des permissions: " + e.getMessage())
.build();
}
}
/** Vérifie si un rôle a une permission spécifique */
@GET
@Path("/check/{role}/{permission}")
public Response checkPermission(
@PathParam("role") String roleStr, @PathParam("permission") String permissionCode) {
try {
logger.debug("GET /api/permissions/check/{}/{}", roleStr, permissionCode);
UserRole role = UserRole.valueOf(roleStr.toUpperCase());
boolean hasPermission = permissionService.hasPermission(role, permissionCode);
return Response.ok(
Map.of(
"role", role.getDisplayName(),
"permission", permissionCode,
"hasPermission", hasPermission))
.build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity("Rôle invalide: " + roleStr)
.build();
} catch (Exception e) {
logger.error("Erreur lors de la vérification de permission", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la vérification de permission: " + e.getMessage())
.build();
}
}
/** Récupère tous les rôles avec leurs permissions */
@GET
@Path("/roles")
public Response getAllRolesWithPermissions() {
try {
logger.debug("GET /api/permissions/roles");
Map<String, Object> result =
Arrays.stream(UserRole.values())
.collect(
Collectors.toMap(
role -> role.name(),
role ->
Map.of(
"displayName", role.getDisplayName(),
"description", role.getDescription(),
"hierarchyLevel", role.getHierarchyLevel(),
"isManagementRole", role.isManagementRole(),
"isFieldRole", role.isFieldRole(),
"isAdministrativeRole", role.isAdministrativeRole(),
"permissions",
permissionService.getPermissions(role).stream()
.map(Permission::getCode)
.collect(Collectors.toList()),
"permissionCount", permissionService.getPermissions(role).size())));
return Response.ok(result).build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération des rôles et permissions", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération des rôles: " + e.getMessage())
.build();
}
}
/** Compare les permissions entre deux rôles */
@GET
@Path("/compare/{role1}/{role2}")
public Response compareRoles(
@PathParam("role1") String role1Str, @PathParam("role2") String role2Str) {
try {
logger.debug("GET /api/permissions/compare/{}/{}", role1Str, role2Str);
UserRole role1 = UserRole.valueOf(role1Str.toUpperCase());
UserRole role2 = UserRole.valueOf(role2Str.toUpperCase());
Set<Permission> permissions1 = permissionService.getPermissions(role1);
Set<Permission> permissions2 = permissionService.getPermissions(role2);
Set<Permission> missing1to2 = permissionService.getMissingPermissions(role1, role2);
Set<Permission> missing2to1 = permissionService.getMissingPermissions(role2, role1);
Set<Permission> common =
permissions1.stream().filter(permissions2::contains).collect(Collectors.toSet());
return Response.ok(
Map.of(
"role1",
Map.of(
"name", role1.getDisplayName(),
"permissionCount", permissions1.size(),
"permissions",
permissions1.stream()
.map(Permission::getCode)
.collect(Collectors.toList())),
"role2",
Map.of(
"name", role2.getDisplayName(),
"permissionCount", permissions2.size(),
"permissions",
permissions2.stream()
.map(Permission::getCode)
.collect(Collectors.toList())),
"common",
Map.of(
"count", common.size(),
"permissions",
common.stream()
.map(Permission::getCode)
.collect(Collectors.toList())),
"onlyInRole1",
Map.of(
"count", missing2to1.size(),
"permissions",
missing2to1.stream()
.map(Permission::getCode)
.collect(Collectors.toList())),
"onlyInRole2",
Map.of(
"count", missing1to2.size(),
"permissions",
missing1to2.stream()
.map(Permission::getCode)
.collect(Collectors.toList()))))
.build();
} catch (IllegalArgumentException e) {
return Response.status(Response.Status.BAD_REQUEST).entity("Rôle invalide").build();
} catch (Exception e) {
logger.error("Erreur lors de la comparaison des rôles", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la comparaison: " + e.getMessage())
.build();
}
}
/** Récupère les permissions spécifiques au gestionnaire de projet */
@GET
@Path("/gestionnaire")
public Response getGestionnairePermissions() {
try {
logger.debug("GET /api/permissions/gestionnaire");
UserRole gestionnaireRole = UserRole.GESTIONNAIRE_PROJET;
Map<String, Object> summary = permissionService.getPermissionSummary(gestionnaireRole);
// Ajout d'informations spécifiques au gestionnaire
Map<PermissionCategory, List<Permission>> byCategory =
permissionService.getPermissionsByCategory(gestionnaireRole);
return Response.ok(
Map.of(
"role", gestionnaireRole.getDisplayName(),
"description", gestionnaireRole.getDescription(),
"summary", summary,
"specificities",
Map.of(
"clientManagement", "Gestion limitée aux clients assignés",
"projectScope", "Chantiers et projets sous sa responsabilité uniquement",
"budgetAccess", "Consultation et planification budgétaire",
"materialReservation", "Réservation de matériel pour ses chantiers",
"reportingLevel", "Rapports et statistiques de ses projets"),
"categoriesDetails",
byCategory.entrySet().stream()
.collect(
Collectors.toMap(
entry -> entry.getKey().getDisplayName(),
entry ->
entry.getValue().stream()
.map(
p ->
Map.of(
"code", p.getCode(),
"description", p.getDescription()))
.collect(Collectors.toList())))))
.build();
} catch (Exception e) {
logger.error("Erreur lors de la récupération des permissions gestionnaire", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la récupération des permissions: " + e.getMessage())
.build();
}
}
/** Valide les permissions requises pour une fonctionnalité */
@POST
@Path("/validate")
public Response validatePermissions(ValidationRequest request) {
try {
logger.debug("POST /api/permissions/validate");
UserRole role = UserRole.valueOf(request.role.toUpperCase());
Set<Permission> requiredPermissions =
request.requiredPermissions.stream()
.map(Permission::fromCode)
.collect(Collectors.toSet());
boolean hasMinimum = permissionService.hasMinimumPermissions(role, requiredPermissions);
Map<String, Boolean> permissionChecks =
request.requiredPermissions.stream()
.collect(
Collectors.toMap(
code -> code, code -> permissionService.hasPermission(role, code)));
return Response.ok(
Map.of(
"role",
role.getDisplayName(),
"hasMinimumPermissions",
hasMinimum,
"permissionChecks",
permissionChecks,
"missingPermissions",
request.requiredPermissions.stream()
.filter(code -> !permissionService.hasPermission(role, code))
.collect(Collectors.toList())))
.build();
} catch (Exception e) {
logger.error("Erreur lors de la validation des permissions", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity("Erreur lors de la validation: " + e.getMessage())
.build();
}
}
// === CLASSES DE REQUÊTE ===
public static class ValidationRequest {
public String role;
public List<String> requiredPermissions;
}
}