AuthenticationFilter.java
/*
* The coLAB project
* Copyright (C) 2021-2023 AlbaSim, MEI, HEIG-VD, HES-SO
*
* Licensed under the MIT License
*/
package ch.colabproject.colab.api.security;
import ch.colabproject.colab.api.controller.RequestManager;
import ch.colabproject.colab.api.model.user.User;
import ch.colabproject.colab.generator.model.annotations.AdminResource;
import ch.colabproject.colab.generator.model.annotations.AuthenticationRequired;
import ch.colabproject.colab.generator.model.annotations.ConsentNotRequired;
import ch.colabproject.colab.generator.model.exceptions.HttpErrorMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Priority;
import javax.inject.Inject;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerRequestFilter;
import javax.ws.rs.container.ResourceInfo;
import javax.ws.rs.core.Context;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
/**
* Intercept all request to the API and check user has required permission.
* <p>
* This filter has a priority of 10, which means it is executed after {@link CookieFilter } and
* {@link ch.colabproject.colab.api.rest.utils.filter.RequestFilter RequestFilter}
*
* @author maxence
*/
@Provider
@Priority(10)
public class AuthenticationFilter implements ContainerRequestFilter {
/**
* Logger
*/
private static final Logger logger = LoggerFactory.getLogger(AuthenticationFilter.class);
/**
* Request related logic
*/
@Inject
private RequestManager requestManager;
/**
* Post-matching filter knows the targeted resource by injecting such a ResourceInfo
*/
@Context
private ResourceInfo resourceInfo;
/**
* To re-use exception to response mapper
*/
@Inject
private ExceptionMapper<Exception> exceptionMapper;
/**
* Cluster-wide session cache.
*/
@Inject
private SessionManager sessionManager;
/**
* To get the last timestamp when the Terms of Use and Data Policy were updated
*/
@Inject
private TermsOfUseManager termsOfUseManager;
/**
* Get all method or class annotations matching the given type.
*
* @param <T> type of annotation to search
* @param annotation type of annotation to search
* @param klass targeted class
* @param method targeted method
* @return the list of all matching annotations found on class and method
*/
private <T extends Annotation> List<T> getAnnotations(Class<T> annotation,
Class<?> klass, Method method) {
List<T> list = new ArrayList<>();
T a = method.getAnnotation(annotation);
if (a != null) {
list.add(a);
}
a = klass.getAnnotation(annotation);
if (a != null) {
list.add(a);
}
return list;
}
/**
* Intercept request and make sure current user has access to targeted class and method
* {@inheritDoc }
*/
@Override
public void filter(ContainerRequestContext requestContext) throws IOException {
// Targeted Class & method
final Class<?> targetClass = resourceInfo.getResourceClass();
Method targetMethod = resourceInfo.getResourceMethod();
User currentUser = requestManager.getCurrentUser();
HttpErrorMessage abortWith = null;
List<AuthenticationRequired> authAnnotations = getAnnotations(
AuthenticationRequired.class,
targetClass, targetMethod);
if (!authAnnotations.isEmpty()) {
if (currentUser == null) {
// current user not authenticated: make sure the targeted method is accessible to
// unauthenticated user
// No current user but annotation required to be authenticated
// abort with 401 code
logger.trace("Request aborted:user is not authenticated");
abortWith = HttpErrorMessage.authenticationRequired();
} else {
List<ConsentNotRequired> consentAnnotations = getAnnotations(
ConsentNotRequired.class,
targetClass, targetMethod);
if (consentAnnotations.isEmpty() && (currentUser.getAgreedTime() == null
|| currentUser.getAgreedTime().isBefore(termsOfUseManager.getTimestamp()))) {
// current user is authenticated but need to accept new TermsOfUse
logger.trace("Request aborted:user has not agreed to new TermsOfUse");
abortWith = HttpErrorMessage.forbidden();
}
}
}
List<AdminResource> adminAnnotations = getAnnotations(
AdminResource.class,
targetClass, targetMethod);
if (!adminAnnotations.isEmpty()) {
if (currentUser == null) {
// no current user : unauthorized asks for user to authenticate
logger.trace("Request aborted:user is not authenticated");
abortWith = HttpErrorMessage.authenticationRequired();
} else {
if (!currentUser.isAdmin()) {
// current user is authenticated but lack admin right: forbidden
logger.trace("Request aborted:user tries to access admin resource");
abortWith = HttpErrorMessage.forbidden();
}
}
}
if (abortWith != null) {
requestContext.abortWith(exceptionMapper.toResponse(abortWith));
} else {
sessionManager.touchUserActivityDate();
}
}
}