/* * 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 { EntityManager em = emProvider.getEntityManager(); LOG.debug("GETTING EM: {}", em); // Store EntityManager for later retrieval (writer interceptor) requestContext.setProperty(EM_CONTEXT_PROPERTY, em); Method method = resourceInfo.getResourceMethod(); if (checkSecurableMethods(requestContext, method)) { if (method.isAnnotationPresent(EnsureTransaction.class)) { LOG.debug("Beginning transaction"); em.getTransaction().begin(); } } } /** * 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.getHeader(TokenHelper.TOKEN_HEADER_PĂ€RAM); 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; } BasicSecurityContext sc = new BasicSecurityContext(username, roles, servletRequest.isSecure()); sc.setOrganizationsIds(getUserOrganizations(username)); sc.setApplicationsIds(getUserApplications(username)); ctx.setSecurityContext(sc); return true; } // ------------------------------------------------------------- // 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) return 0; Integer cached = cache.get("roles_" + username, Integer.class); if (cached != null) return cached; EntityManager em = emProvider.getEntityManager(); 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); // also warm some caches 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) { Set cached = cache.getSet("orgs_" + username, Integer.class); if (cached != null) return cached; User user = emProvider.getEntityManager().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) { Set cached = cache.getSet("apps_" + username, Integer.class); if (cached != null) return cached; User user = emProvider.getEntityManager().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 { context.proceed(); EntityManager em = (EntityManager) context.getProperty(EM_CONTEXT_PROPERTY); if (em == null) return; try { if (em.getTransaction().isActive()) { if (servletResponse.getStatus() == Status.OK.getStatusCode()) { em.getTransaction().commit(); LOG.debug("Transaction committed"); } else { em.getTransaction().rollback(); LOG.debug("Transaction rolled back"); } } } finally { if (em.isOpen()) { try { em.close(); } catch (Exception e) { LOG.error("Error closing EntityManager", e); } } } } }