/* * 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
* 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
* Retrieve organization scope for the user as a typed {@code Set
* Retrieve application scope for the user as a typed {@code Set
* 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);
}
}
}
}
}