| .. | .. |
|---|
| 1 | +/* |
|---|
| 2 | +* Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved. |
|---|
| 3 | +*/ |
|---|
| 1 | 4 | package net.curisit.securis.ioc; |
|---|
| 2 | 5 | |
|---|
| 3 | 6 | import java.io.IOException; |
|---|
| .. | .. |
|---|
| 31 | 34 | import net.curisit.securis.utils.CacheTTL; |
|---|
| 32 | 35 | import net.curisit.securis.utils.TokenHelper; |
|---|
| 33 | 36 | |
|---|
| 37 | +/** |
|---|
| 38 | +* RequestsInterceptor |
|---|
| 39 | +* <p> |
|---|
| 40 | +* Authentication/authorization interceptor that: |
|---|
| 41 | +* <ul> |
|---|
| 42 | +* <li>Loads and stores the {@link EntityManager} in the request context.</li> |
|---|
| 43 | +* <li>Validates tokens for methods annotated with {@link Securable}.</li> |
|---|
| 44 | +* <li>Builds a {@link BasicSecurityContext} with roles and scoped organization/application IDs.</li> |
|---|
| 45 | +* <li>Manages transactions when {@code @EnsureTransaction} is present.</li> |
|---|
| 46 | +* </ul> |
|---|
| 47 | +* |
|---|
| 48 | +* <p><b>Cache usage:</b> Uses {@link CacheTTL} to cache roles and scope sets. |
|---|
| 49 | +* The new {@link CacheTTL#getSet(String, Class)} helper removes unchecked |
|---|
| 50 | +* conversion warnings when retrieving {@code Set<Integer>} from the cache. |
|---|
| 51 | +* |
|---|
| 52 | +* @author JRA |
|---|
| 53 | +* Last reviewed by JRA on Oct 5, 2025. |
|---|
| 54 | +*/ |
|---|
| 34 | 55 | @Provider |
|---|
| 35 | 56 | @Priority(Priorities.AUTHENTICATION) |
|---|
| 36 | 57 | public class RequestsInterceptor implements ContainerRequestFilter, WriterInterceptor { |
|---|
| 37 | 58 | |
|---|
| 38 | | - private static final Logger LOG = LogManager.getLogger(RequestsInterceptor.class); |
|---|
| 59 | + private static final Logger LOG = LogManager.getLogger(RequestsInterceptor.class); |
|---|
| 39 | 60 | |
|---|
| 40 | | - @Context |
|---|
| 41 | | - private HttpServletResponse servletResponse; |
|---|
| 61 | + @Inject private CacheTTL cache; |
|---|
| 62 | + @Inject private TokenHelper tokenHelper; |
|---|
| 63 | + @Inject private EntityManagerProvider emProvider; |
|---|
| 42 | 64 | |
|---|
| 43 | | - @Context |
|---|
| 44 | | - private HttpServletRequest servletRequest; |
|---|
| 65 | + @Context private HttpServletResponse servletResponse; |
|---|
| 66 | + @Context private HttpServletRequest servletRequest; |
|---|
| 67 | + @Context private ResourceInfo resourceInfo; |
|---|
| 45 | 68 | |
|---|
| 46 | | - @Context |
|---|
| 47 | | - private ResourceInfo resourceInfo; |
|---|
| 69 | + private static final String EM_CONTEXT_PROPERTY = "curisit.entitymanager"; |
|---|
| 48 | 70 | |
|---|
| 49 | | - @Inject |
|---|
| 50 | | - private CacheTTL cache; |
|---|
| 71 | + // ------------------------------------------------------------- |
|---|
| 72 | + // Request filter (authN/authZ + EM handling) |
|---|
| 73 | + // ------------------------------------------------------------- |
|---|
| 51 | 74 | |
|---|
| 52 | | - @Inject |
|---|
| 53 | | - private TokenHelper tokenHelper; |
|---|
| 75 | + /** |
|---|
| 76 | + * filter<p> |
|---|
| 77 | + * Entry point before resource method invocation. |
|---|
| 78 | + * |
|---|
| 79 | + * @param requestContext |
|---|
| 80 | + * @throws IOException |
|---|
| 81 | + */ |
|---|
| 82 | + @Override |
|---|
| 83 | + public void filter(ContainerRequestContext requestContext) throws IOException { |
|---|
| 84 | + EntityManager em = emProvider.getEntityManager(); |
|---|
| 85 | + LOG.debug("GETTING EM: {}", em); |
|---|
| 54 | 86 | |
|---|
| 55 | | - @Inject |
|---|
| 56 | | - private EntityManagerProvider emProvider; |
|---|
| 87 | + // Store EntityManager for later retrieval (writer interceptor) |
|---|
| 88 | + requestContext.setProperty(EM_CONTEXT_PROPERTY, em); |
|---|
| 57 | 89 | |
|---|
| 58 | | - private static final String EM_CONTEXT_PROPERTY = "curisit.entitymanager"; |
|---|
| 90 | + Method method = resourceInfo.getResourceMethod(); |
|---|
| 59 | 91 | |
|---|
| 60 | | - @Override |
|---|
| 61 | | - public void filter(ContainerRequestContext requestContext) throws IOException { |
|---|
| 62 | | - EntityManager em = emProvider.getEntityManager(); |
|---|
| 63 | | - LOG.debug("GETTING EM: {}", em); |
|---|
| 92 | + if (checkSecurableMethods(requestContext, method)) { |
|---|
| 93 | + if (method.isAnnotationPresent(EnsureTransaction.class)) { |
|---|
| 94 | + LOG.debug("Beginning transaction"); |
|---|
| 95 | + em.getTransaction().begin(); |
|---|
| 96 | + } |
|---|
| 97 | + } |
|---|
| 98 | + } |
|---|
| 64 | 99 | |
|---|
| 65 | | - // Guardamos el EntityManager en el contexto para recuperación posterior |
|---|
| 66 | | - requestContext.setProperty(EM_CONTEXT_PROPERTY, em); |
|---|
| 100 | + /** |
|---|
| 101 | + * checkSecurableMethods<p> |
|---|
| 102 | + * Enforce security checks for methods annotated with {@link Securable}. |
|---|
| 103 | + * Builds {@link BasicSecurityContext} when authorized. |
|---|
| 104 | + * |
|---|
| 105 | + * @param ctx |
|---|
| 106 | + * @param method |
|---|
| 107 | + * @return true if request can proceed; false when aborted |
|---|
| 108 | + */ |
|---|
| 109 | + private boolean checkSecurableMethods(ContainerRequestContext ctx, Method method) { |
|---|
| 110 | + if (!method.isAnnotationPresent(Securable.class)) return true; |
|---|
| 67 | 111 | |
|---|
| 68 | | - Method method = resourceInfo.getResourceMethod(); |
|---|
| 112 | + String token = servletRequest.getHeader(TokenHelper.TOKEN_HEADER_PÀRAM); |
|---|
| 113 | + if (token == null || !tokenHelper.isTokenValid(token)) { |
|---|
| 114 | + LOG.warn("Access denied, invalid token"); |
|---|
| 115 | + ctx.abortWith(Response.status(Status.UNAUTHORIZED).build()); |
|---|
| 116 | + return false; |
|---|
| 117 | + } |
|---|
| 69 | 118 | |
|---|
| 70 | | - if (checkSecurableMethods(requestContext, method)) { |
|---|
| 71 | | - if (method.isAnnotationPresent(EnsureTransaction.class)) { |
|---|
| 72 | | - LOG.debug("Beginning transaction"); |
|---|
| 73 | | - em.getTransaction().begin(); |
|---|
| 74 | | - } |
|---|
| 75 | | - } |
|---|
| 76 | | - } |
|---|
| 119 | + String username = tokenHelper.extractUserFromToken(token); |
|---|
| 120 | + int roles = getUserRoles(username); |
|---|
| 121 | + Securable securable = method.getAnnotation(Securable.class); |
|---|
| 77 | 122 | |
|---|
| 78 | | - private boolean checkSecurableMethods(ContainerRequestContext ctx, Method method) { |
|---|
| 79 | | - if (!method.isAnnotationPresent(Securable.class)) return true; |
|---|
| 123 | + if (securable.roles() != 0 && (securable.roles() & roles) == 0) { |
|---|
| 124 | + LOG.warn("User {} lacks required roles for method {}", username, method.getName()); |
|---|
| 125 | + ctx.abortWith(Response.status(Status.UNAUTHORIZED).build()); |
|---|
| 126 | + return false; |
|---|
| 127 | + } |
|---|
| 80 | 128 | |
|---|
| 81 | | - String token = servletRequest.getHeader(TokenHelper.TOKEN_HEADER_PÀRAM); |
|---|
| 82 | | - if (token == null || !tokenHelper.isTokenValid(token)) { |
|---|
| 83 | | - LOG.warn("Access denied, invalid token"); |
|---|
| 84 | | - ctx.abortWith(Response.status(Status.UNAUTHORIZED).build()); |
|---|
| 85 | | - return false; |
|---|
| 86 | | - } |
|---|
| 129 | + BasicSecurityContext sc = new BasicSecurityContext(username, roles, servletRequest.isSecure()); |
|---|
| 130 | + sc.setOrganizationsIds(getUserOrganizations(username)); |
|---|
| 131 | + sc.setApplicationsIds(getUserApplications(username)); |
|---|
| 132 | + ctx.setSecurityContext(sc); |
|---|
| 133 | + return true; |
|---|
| 134 | + } |
|---|
| 87 | 135 | |
|---|
| 88 | | - String username = tokenHelper.extractUserFromToken(token); |
|---|
| 89 | | - int roles = getUserRoles(username); |
|---|
| 90 | | - Securable securable = method.getAnnotation(Securable.class); |
|---|
| 136 | + // ------------------------------------------------------------- |
|---|
| 137 | + // Cached lookups (roles/orgs/apps) |
|---|
| 138 | + // ------------------------------------------------------------- |
|---|
| 91 | 139 | |
|---|
| 92 | | - if (securable.roles() != 0 && (securable.roles() & roles) == 0) { |
|---|
| 93 | | - LOG.warn("User {} lacks required roles for method {}", username, method.getName()); |
|---|
| 94 | | - ctx.abortWith(Response.status(Status.UNAUTHORIZED).build()); |
|---|
| 95 | | - return false; |
|---|
| 96 | | - } |
|---|
| 140 | + /** |
|---|
| 141 | + * getUserRoles<p> |
|---|
| 142 | + * Retrieve roles bitmask for the given user (cached). |
|---|
| 143 | + * |
|---|
| 144 | + * @param username |
|---|
| 145 | + * @return userRoles |
|---|
| 146 | + */ |
|---|
| 147 | + private int getUserRoles(String username) { |
|---|
| 148 | + if (username == null) return 0; |
|---|
| 149 | + Integer cached = cache.get("roles_" + username, Integer.class); |
|---|
| 150 | + if (cached != null) return cached; |
|---|
| 97 | 151 | |
|---|
| 98 | | - BasicSecurityContext sc = new BasicSecurityContext(username, roles, servletRequest.isSecure()); |
|---|
| 99 | | - sc.setOrganizationsIds(getUserOrganizations(username)); |
|---|
| 100 | | - sc.setApplicationsIds(getUserApplications(username)); |
|---|
| 101 | | - ctx.setSecurityContext(sc); |
|---|
| 102 | | - return true; |
|---|
| 103 | | - } |
|---|
| 152 | + EntityManager em = emProvider.getEntityManager(); |
|---|
| 153 | + User user = em.find(User.class, username); |
|---|
| 154 | + int roles = 0; |
|---|
| 155 | + if (user != null) { |
|---|
| 156 | + List<Integer> r = user.getRoles(); |
|---|
| 157 | + if (r != null) for (Integer role : r) roles += role; |
|---|
| 158 | + cache.set("roles_" + username, roles, 3600); |
|---|
| 159 | + // also warm some caches |
|---|
| 160 | + cache.set("orgs_" + username, user.getOrgsIds(), 3600); |
|---|
| 161 | + } |
|---|
| 162 | + return roles; |
|---|
| 163 | + } |
|---|
| 104 | 164 | |
|---|
| 105 | | - private int getUserRoles(String username) { |
|---|
| 106 | | - if (username == null) return 0; |
|---|
| 107 | | - Integer cached = cache.get("roles_" + username, Integer.class); |
|---|
| 108 | | - if (cached != null) return cached; |
|---|
| 165 | + /** |
|---|
| 166 | + * getUserOrganizations<p> |
|---|
| 167 | + * Retrieve organization scope for the user as a typed {@code Set<Integer>} |
|---|
| 168 | + * using the cache helper that validates element types. |
|---|
| 169 | + * |
|---|
| 170 | + * @param username |
|---|
| 171 | + * @return userOrganizations |
|---|
| 172 | + */ |
|---|
| 173 | + private Set<Integer> getUserOrganizations(String username) { |
|---|
| 174 | + Set<Integer> cached = cache.getSet("orgs_" + username, Integer.class); |
|---|
| 175 | + if (cached != null) return cached; |
|---|
| 109 | 176 | |
|---|
| 110 | | - EntityManager em = emProvider.getEntityManager(); |
|---|
| 111 | | - User user = em.find(User.class, username); |
|---|
| 112 | | - int roles = 0; |
|---|
| 113 | | - if (user != null) { |
|---|
| 114 | | - List<Integer> r = user.getRoles(); |
|---|
| 115 | | - if (r != null) for (Integer role : r) roles += role; |
|---|
| 116 | | - cache.set("roles_" + username, roles, 3600); |
|---|
| 117 | | - cache.set("orgs_" + username, user.getOrgsIds(), 3600); |
|---|
| 118 | | - } |
|---|
| 119 | | - return roles; |
|---|
| 120 | | - } |
|---|
| 177 | + User user = emProvider.getEntityManager().find(User.class, username); |
|---|
| 178 | + if (user != null) { |
|---|
| 179 | + Set<Integer> result = user.getAllOrgsIds(); |
|---|
| 180 | + cache.set("orgs_" + username, result, 3600); |
|---|
| 181 | + return result; |
|---|
| 182 | + } |
|---|
| 183 | + return Set.of(); |
|---|
| 184 | + } |
|---|
| 121 | 185 | |
|---|
| 122 | | - private Set<Integer> getUserOrganizations(String username) { |
|---|
| 123 | | - Set<Integer> cached = cache.get("orgs_" + username, Set.class); |
|---|
| 124 | | - if (cached != null) return cached; |
|---|
| 125 | | - User user = emProvider.getEntityManager().find(User.class, username); |
|---|
| 126 | | - if (user != null) { |
|---|
| 127 | | - Set<Integer> result = user.getAllOrgsIds(); |
|---|
| 128 | | - cache.set("orgs_" + username, result, 3600); |
|---|
| 129 | | - return result; |
|---|
| 130 | | - } |
|---|
| 131 | | - return Set.of(); |
|---|
| 132 | | - } |
|---|
| 186 | + /** |
|---|
| 187 | + * getUserApplications<p> |
|---|
| 188 | + * Retrieve application scope for the user as a typed {@code Set<Integer>} |
|---|
| 189 | + * using the cache helper that validates element types. |
|---|
| 190 | + * |
|---|
| 191 | + * @param username |
|---|
| 192 | + * @return userApplications |
|---|
| 193 | + */ |
|---|
| 194 | + private Set<Integer> getUserApplications(String username) { |
|---|
| 195 | + Set<Integer> cached = cache.getSet("apps_" + username, Integer.class); |
|---|
| 196 | + if (cached != null) return cached; |
|---|
| 133 | 197 | |
|---|
| 134 | | - private Set<Integer> getUserApplications(String username) { |
|---|
| 135 | | - Set<Integer> cached = cache.get("apps_" + username, Set.class); |
|---|
| 136 | | - if (cached != null) return cached; |
|---|
| 137 | | - User user = emProvider.getEntityManager().find(User.class, username); |
|---|
| 138 | | - if (user != null) { |
|---|
| 139 | | - Set<Integer> result = user.getAllAppsIds(); |
|---|
| 140 | | - cache.set("apps_" + username, result, 3600); |
|---|
| 141 | | - return result; |
|---|
| 142 | | - } |
|---|
| 143 | | - return Set.of(); |
|---|
| 144 | | - } |
|---|
| 198 | + User user = emProvider.getEntityManager().find(User.class, username); |
|---|
| 199 | + if (user != null) { |
|---|
| 200 | + Set<Integer> result = user.getAllAppsIds(); |
|---|
| 201 | + cache.set("apps_" + username, result, 3600); |
|---|
| 202 | + return result; |
|---|
| 203 | + } |
|---|
| 204 | + return Set.of(); |
|---|
| 205 | + } |
|---|
| 145 | 206 | |
|---|
| 146 | | - @Override |
|---|
| 147 | | - public void aroundWriteTo(WriterInterceptorContext context) throws IOException, WebApplicationException { |
|---|
| 148 | | - context.proceed(); |
|---|
| 207 | + // ------------------------------------------------------------- |
|---|
| 208 | + // Writer interceptor (transaction finalize) |
|---|
| 209 | + // ------------------------------------------------------------- |
|---|
| 149 | 210 | |
|---|
| 150 | | - EntityManager em = (EntityManager) context.getProperty(EM_CONTEXT_PROPERTY); |
|---|
| 151 | | - if (em == null) return; |
|---|
| 211 | + /** |
|---|
| 212 | + * aroundWriteTo<p> |
|---|
| 213 | + * Commit/rollback and close EM after response writing. |
|---|
| 214 | + * |
|---|
| 215 | + * @param context |
|---|
| 216 | + * @throws IOException |
|---|
| 217 | + * @throws WebApplicationException |
|---|
| 218 | + */ |
|---|
| 219 | + @Override |
|---|
| 220 | + public void aroundWriteTo(WriterInterceptorContext context) throws IOException, WebApplicationException { |
|---|
| 221 | + context.proceed(); |
|---|
| 152 | 222 | |
|---|
| 153 | | - try { |
|---|
| 154 | | - if (em.getTransaction().isActive()) { |
|---|
| 155 | | - if (servletResponse.getStatus() == Status.OK.getStatusCode()) { |
|---|
| 156 | | - em.getTransaction().commit(); |
|---|
| 157 | | - LOG.debug("Transaction committed"); |
|---|
| 158 | | - } else { |
|---|
| 159 | | - em.getTransaction().rollback(); |
|---|
| 160 | | - LOG.debug("Transaction rolled back"); |
|---|
| 161 | | - } |
|---|
| 162 | | - } |
|---|
| 163 | | - } finally { |
|---|
| 164 | | - if (em.isOpen()) { |
|---|
| 165 | | - try { |
|---|
| 166 | | - em.close(); |
|---|
| 167 | | - } catch (Exception e) { |
|---|
| 168 | | - LOG.error("Error closing EntityManager", e); |
|---|
| 169 | | - } |
|---|
| 170 | | - } |
|---|
| 171 | | - } |
|---|
| 172 | | - } |
|---|
| 223 | + EntityManager em = (EntityManager) context.getProperty(EM_CONTEXT_PROPERTY); |
|---|
| 224 | + if (em == null) return; |
|---|
| 225 | + |
|---|
| 226 | + try { |
|---|
| 227 | + if (em.getTransaction().isActive()) { |
|---|
| 228 | + if (servletResponse.getStatus() == Status.OK.getStatusCode()) { |
|---|
| 229 | + em.getTransaction().commit(); |
|---|
| 230 | + LOG.debug("Transaction committed"); |
|---|
| 231 | + } else { |
|---|
| 232 | + em.getTransaction().rollback(); |
|---|
| 233 | + LOG.debug("Transaction rolled back"); |
|---|
| 234 | + } |
|---|
| 235 | + } |
|---|
| 236 | + } finally { |
|---|
| 237 | + if (em.isOpen()) { |
|---|
| 238 | + try { |
|---|
| 239 | + em.close(); |
|---|
| 240 | + } catch (Exception e) { |
|---|
| 241 | + LOG.error("Error closing EntityManager", e); |
|---|
| 242 | + } |
|---|
| 243 | + } |
|---|
| 244 | + } |
|---|
| 245 | + } |
|---|
| 173 | 246 | } |
|---|
| 247 | + |
|---|