/* * Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved. */ package net.curisit.securis.ioc; import java.io.IOException; import java.lang.reflect.Method; import java.util.List; import java.util.Set; import jakarta.annotation.Priority; import jakarta.inject.Inject; import jakarta.persistence.EntityManager; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.ws.rs.Priorities; import jakarta.ws.rs.WebApplicationException; import jakarta.ws.rs.container.ContainerRequestContext; import jakarta.ws.rs.container.ContainerRequestFilter; import jakarta.ws.rs.container.ResourceInfo; import jakarta.ws.rs.core.Context; import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response.Status; import jakarta.ws.rs.ext.Provider; import jakarta.ws.rs.ext.WriterInterceptor; import jakarta.ws.rs.ext.WriterInterceptorContext; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import net.curisit.securis.db.User; import net.curisit.securis.security.BasicSecurityContext; import net.curisit.securis.security.Securable; import net.curisit.securis.utils.CacheTTL; import net.curisit.securis.utils.TokenHelper; /** * RequestsInterceptor *

* Authentication/authorization interceptor that: *

* *

Cache usage: Uses {@link CacheTTL} to cache roles and scope sets. * The new {@link CacheTTL#getSet(String, Class)} helper removes unchecked * conversion warnings when retrieving {@code Set} from the cache. * * @author JRA * Last reviewed by JRA on Oct 5, 2025. */ @Provider @Priority(Priorities.AUTHENTICATION) public class RequestsInterceptor implements ContainerRequestFilter, WriterInterceptor { private static final Logger LOG = LogManager.getLogger(RequestsInterceptor.class); @Inject private CacheTTL cache; @Inject private TokenHelper tokenHelper; @Inject private EntityManagerProvider emProvider; @Context private HttpServletResponse servletResponse; @Context private HttpServletRequest servletRequest; @Context private ResourceInfo resourceInfo; private static final String EM_CONTEXT_PROPERTY = "curisit.entitymanager"; // ------------------------------------------------------------- // Request filter (authN/authZ + EM handling) // ------------------------------------------------------------- /** * filter

* Entry point before resource method invocation. * * @param requestContext * @throws IOException */ @Override public void filter(ContainerRequestContext requestContext) throws IOException { Method method = resourceInfo != null ? resourceInfo.getResourceMethod() : null; if (method == null) { LOG.warn("RequestsInterceptor: resource method is null"); return; } boolean securable = method.isAnnotationPresent(Securable.class); boolean ensureTransaction = method.isAnnotationPresent(EnsureTransaction.class); // Only require injected helpers when the endpoint actually needs them if (securable) { if (tokenHelper == null || cache == null || emProvider == null) { LOG.error( "RequestsInterceptor is not fully initialized for secured endpoint '{}'. " + "tokenHelper={}, cache={}, emProvider={}", method.getName(), tokenHelper, cache, emProvider ); requestContext.abortWith( Response.status(Status.INTERNAL_SERVER_ERROR) .entity("Security infrastructure not initialized") .build() ); return; } if (!checkSecurableMethods(requestContext, method)) { return; } } // Only open/use EM when needed if (ensureTransaction || securable) { EntityManager em = getEntityManagerSafely(); if (em == null) { LOG.error("No EntityManager available for method '{}'", method.getName()); requestContext.abortWith( Response.status(Status.INTERNAL_SERVER_ERROR) .entity("Persistence infrastructure not initialized") .build() ); return; } LOG.debug("GETTING EM: {}", em); requestContext.setProperty(EM_CONTEXT_PROPERTY, em); if (ensureTransaction) { try { if (!em.getTransaction().isActive()) { LOG.debug("Beginning transaction"); em.getTransaction().begin(); } } catch (Exception e) { LOG.error("Error beginning transaction for method '{}'", method.getName(), e); requestContext.abortWith( Response.status(Status.INTERNAL_SERVER_ERROR) .entity("Could not begin transaction") .build() ); } } } } /** * checkSecurableMethods

* Enforce security checks for methods annotated with {@link Securable}. * Builds {@link BasicSecurityContext} when authorized. * * @param ctx * @param method * @return true if request can proceed; false when aborted */ private boolean checkSecurableMethods(ContainerRequestContext ctx, Method method) { if (!method.isAnnotationPresent(Securable.class)) { return true; } String token = servletRequest != null ? servletRequest.getHeader(TokenHelper.TOKEN_HEADER_PĂ€RAM) : null; if (token == null || !tokenHelper.isTokenValid(token)) { LOG.warn("Access denied, invalid token"); ctx.abortWith(Response.status(Status.UNAUTHORIZED).build()); return false; } String username = tokenHelper.extractUserFromToken(token); int roles = getUserRoles(username); Securable securable = method.getAnnotation(Securable.class); if (securable.roles() != 0 && (securable.roles() & roles) == 0) { LOG.warn("User {} lacks required roles for method {}", username, method.getName()); ctx.abortWith(Response.status(Status.UNAUTHORIZED).build()); return false; } boolean secure = servletRequest != null && servletRequest.isSecure(); BasicSecurityContext sc = new BasicSecurityContext(username, roles, secure); sc.setOrganizationsIds(getUserOrganizations(username)); sc.setApplicationsIds(getUserApplications(username)); ctx.setSecurityContext(sc); return true; } /** * getEntityManagerSafely

* Get the entity manager in a safely way * * @return entityManager */ private EntityManager getEntityManagerSafely() { try { if (emProvider == null) { return null; } return emProvider.getEntityManager(); } catch (Exception e) { LOG.error("Error obtaining EntityManager from provider", e); return null; } } // ------------------------------------------------------------- // Cached lookups (roles/orgs/apps) // ------------------------------------------------------------- /** * getUserRoles

* Retrieve roles bitmask for the given user (cached). * * @param username * @return userRoles */ private int getUserRoles(String username) { if (username == null || cache == null) { return 0; } Integer cached = cache.get("roles_" + username, Integer.class); if (cached != null) { return cached; } EntityManager em = getEntityManagerSafely(); if (em == null) { LOG.error("Cannot resolve user roles: EntityManager is not available"); return 0; } User user = em.find(User.class, username); int roles = 0; if (user != null) { List r = user.getRoles(); if (r != null) { for (Integer role : r) { roles += role; } } cache.set("roles_" + username, roles, 3600); cache.set("orgs_" + username, user.getOrgsIds(), 3600); } return roles; } /** * getUserOrganizations

* Retrieve organization scope for the user as a typed {@code Set} * using the cache helper that validates element types. * * @param username * @return userOrganizations */ private Set getUserOrganizations(String username) { if (username == null || cache == null) { return Set.of(); } Set cached = cache.getSet("orgs_" + username, Integer.class); if (cached != null) { return cached; } EntityManager em = getEntityManagerSafely(); if (em == null) { LOG.error("Cannot resolve user organizations: EntityManager is not available"); return Set.of(); } User user = em.find(User.class, username); if (user != null) { Set result = user.getAllOrgsIds(); cache.set("orgs_" + username, result, 3600); return result; } return Set.of(); } /** * getUserApplications

* Retrieve application scope for the user as a typed {@code Set} * using the cache helper that validates element types. * * @param username * @return userApplications */ private Set getUserApplications(String username) { if (username == null || cache == null) { return Set.of(); } Set cached = cache.getSet("apps_" + username, Integer.class); if (cached != null) { return cached; } EntityManager em = getEntityManagerSafely(); if (em == null) { LOG.error("Cannot resolve user applications: EntityManager is not available"); return Set.of(); } User user = em.find(User.class, username); if (user != null) { Set result = user.getAllAppsIds(); cache.set("apps_" + username, result, 3600); return result; } return Set.of(); } // ------------------------------------------------------------- // Writer interceptor (transaction finalize) // ------------------------------------------------------------- /** * aroundWriteTo

* Commit/rollback and close EM after response writing. * * @param context * @throws IOException * @throws WebApplicationException */ @Override public void aroundWriteTo(WriterInterceptorContext context) throws IOException, WebApplicationException { try { context.proceed(); } finally { EntityManager em = (EntityManager) context.getProperty(EM_CONTEXT_PROPERTY); if (em == null) { return; } try { if (em.getTransaction() != null && em.getTransaction().isActive()) { int status = servletResponse != null ? servletResponse.getStatus() : Status.INTERNAL_SERVER_ERROR.getStatusCode(); if (status >= 200 && status < 300) { em.getTransaction().commit(); LOG.debug("Transaction committed"); } else { em.getTransaction().rollback(); LOG.debug("Transaction rolled back"); } } } catch (Exception e) { LOG.error("Error finalizing transaction", e); try { if (em.getTransaction() != null && em.getTransaction().isActive()) { em.getTransaction().rollback(); } } catch (Exception rollbackEx) { LOG.error("Error rolling back transaction", rollbackEx); } } finally { try { if (em.isOpen()) { em.close(); } } catch (Exception e) { LOG.error("Error closing EntityManager", e); } } } } }