argos/dmp-backend/web/src/main/java/eu/eudat/interceptors/tenant/TenantInterceptor.java

242 lines
10 KiB
Java

package eu.eudat.interceptors.tenant;
import eu.eudat.authorization.Permission;
import eu.eudat.commons.enums.IsActive;
import eu.eudat.commons.lock.LockByKeyManager;
import eu.eudat.commons.scope.tenant.TenantScope;
import eu.eudat.commons.scope.user.UserScope;
import eu.eudat.data.TenantUserEntity;
import eu.eudat.data.UserEntity;
import eu.eudat.data.tenant.TenantScopedBaseEntity;
import eu.eudat.errorcode.ErrorThesaurusProperties;
import eu.eudat.query.utils.BuildSubQueryInput;
import eu.eudat.query.utils.QueryUtilsService;
import gr.cite.commons.web.authz.service.AuthorizationService;
import gr.cite.commons.web.oidc.principal.CurrentPrincipalResolver;
import gr.cite.commons.web.oidc.principal.extractor.ClaimExtractor;
import gr.cite.tools.exception.MyForbiddenException;
import gr.cite.tools.logging.LoggerService;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import jakarta.persistence.Tuple;
import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.criteria.Root;
import org.hibernate.Session;
import org.jetbrains.annotations.NotNull;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;
import org.springframework.ui.ModelMap;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.context.request.WebRequestInterceptor;
import javax.management.InvalidApplicationException;
import java.time.Instant;
import java.util.List;
import java.util.Locale;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@Component
public class TenantInterceptor implements WebRequestInterceptor {
private static final LoggerService logger = new LoggerService(LoggerFactory.getLogger(TenantInterceptor.class));
private final TenantScope tenantScope;
private final UserScope userScope;
private final CurrentPrincipalResolver currentPrincipalResolver;
private final ClaimExtractor claimExtractor;
private final ApplicationContext applicationContext;
private final TenantScopeProperties tenantScopeProperties;
private final UserAllowedTenantCacheService userAllowedTenantCacheService;
private final PlatformTransactionManager transactionManager;
private final ErrorThesaurusProperties errors;
private final QueryUtilsService queryUtilsService;
private final LockByKeyManager lockByKeyManager;
@PersistenceContext
public EntityManager entityManager;
@Autowired
public TenantInterceptor(
TenantScope tenantScope,
UserScope userScope,
CurrentPrincipalResolver currentPrincipalResolver,
ClaimExtractor claimExtractor,
ApplicationContext applicationContext,
TenantScopeProperties tenantScopeProperties,
UserAllowedTenantCacheService userAllowedTenantCacheService,
PlatformTransactionManager transactionManager,
ErrorThesaurusProperties errors, QueryUtilsService queryUtilsService, LockByKeyManager lockByKeyManager) {
this.tenantScope = tenantScope;
this.userScope = userScope;
this.currentPrincipalResolver = currentPrincipalResolver;
this.claimExtractor = claimExtractor;
this.applicationContext = applicationContext;
this.tenantScopeProperties = tenantScopeProperties;
this.userAllowedTenantCacheService = userAllowedTenantCacheService;
this.transactionManager = transactionManager;
this.errors = errors;
this.queryUtilsService = queryUtilsService;
this.lockByKeyManager = lockByKeyManager;
}
@Override
public void preHandle(@NotNull WebRequest request) throws InvalidApplicationException, InterruptedException {
if (!this.currentPrincipalResolver.currentPrincipal().isAuthenticated()) return;
if (!this.tenantScope.isMultitenant()) return;
boolean isAllowedNoTenant = this.applicationContext.getBean(AuthorizationService.class).authorize(Permission.AllowNoTenant);
if (tenantScope.isSet() && this.entityManager != null) {
List<String> currentPrincipalTenantCodes = this.claimExtractor.asStrings(this.currentPrincipalResolver.currentPrincipal(), TenantScope.TenantCodesClaimName);
if ((currentPrincipalTenantCodes == null || !currentPrincipalTenantCodes.contains(tenantScope.getTenantCode())) && !isAllowedNoTenant) {
logger.warn("tenant not allowed {}", this.tenantScope.getTenant());
throw new MyForbiddenException(this.errors.getTenantNotAllowed().getCode(), this.errors.getTenantNotAllowed().getMessage());
}
boolean isUserAllowedTenant = false;
if (this.tenantScope.isDefaultTenant()){
isUserAllowedTenant = true;
} else {
UserAllowedTenantCacheService.UserAllowedTenantCacheValue cacheValue = this.userAllowedTenantCacheService.lookup(this.userAllowedTenantCacheService.buildKey(this.userScope.getUserId(), this.tenantScope.getTenant()));
if (cacheValue != null) {
isUserAllowedTenant = cacheValue.isAllowed();
} else {
isUserAllowedTenant = this.isUserAllowedTenant();
this.userAllowedTenantCacheService.put(new UserAllowedTenantCacheService.UserAllowedTenantCacheValue(this.userScope.getUserId(), this.tenantScope.getTenant(), isUserAllowedTenant));
}
}
if (isUserAllowedTenant) {
if(!tenantScope.isDefaultTenant()) {
this.entityManager
.unwrap(Session.class)
.enableFilter(TenantScopedBaseEntity.TENANT_FILTER)
.setParameter(TenantScopedBaseEntity.TENANT_FILTER_TENANT_PARAM, tenantScope.getTenant().toString());
} else {
this.entityManager
.unwrap(Session.class)
.enableFilter(TenantScopedBaseEntity.DEFAULT_TENANT_FILTER);
}
} else {
if (isAllowedNoTenant || this.isWhiteListedEndpoint(request)) {
tenantScope.setTenant(null, null);
} else {
logger.warn("tenant not allowed {}", this.tenantScope.getTenant());
throw new MyForbiddenException(this.errors.getTenantNotAllowed().getCode(), this.errors.getTenantNotAllowed().getMessage());
}
}
} else {
if (!isAllowedNoTenant) {
if (!this.isWhiteListedEndpoint(request)) {
logger.warn("tenant scope not provided");
throw new MyForbiddenException(this.errors.getMissingTenant().getCode(), this.errors.getMissingTenant().getMessage());
}
}
}
}
private boolean isWhiteListedEndpoint(WebRequest request) {
String servletPath = ((ServletWebRequest) request).getRequest().getServletPath();
if (this.tenantScopeProperties.getWhiteListedEndpoints() != null) {
for (String whiteListedEndpoint : this.tenantScopeProperties.getWhiteListedEndpoints()) {
if (servletPath.toLowerCase(Locale.ROOT).startsWith(whiteListedEndpoint.toLowerCase(Locale.ROOT))) {
return true;
}
}
}
return false;
}
private boolean isUserAllowedTenant() throws InvalidApplicationException, InterruptedException {
if (userScope.isSet()) {
boolean usedResource = false;
String lockId = userScope.getUserId().toString().toLowerCase(Locale.ROOT);
try {
if (this.tenantScopeProperties.getAutoCreateTenantUser()) usedResource = this.lockByKeyManager.tryLock(lockId, 5000, TimeUnit.MILLISECONDS);
CriteriaBuilder criteriaBuilder = this.entityManager.getCriteriaBuilder();
CriteriaQuery<Tuple> query = criteriaBuilder.createQuery(Tuple.class);
Root<UserEntity> root = query.from(UserEntity.class);
query.where(criteriaBuilder.and(
criteriaBuilder.equal(root.get(UserEntity._isActive), IsActive.Active),
criteriaBuilder.in(root.get(UserEntity._id)).value(queryUtilsService.buildSubQuery(new BuildSubQueryInput<>(new BuildSubQueryInput.Builder<>(TenantUserEntity.class, UUID.class)
.query(query)
.criteriaBuilder(criteriaBuilder)
.keyPathFunc((subQueryRoot) -> subQueryRoot.get(TenantUserEntity._userId))
.filterFunc((subQueryRoot, cb) ->
{
try {
return cb.and(
criteriaBuilder.equal(subQueryRoot.get(TenantUserEntity._tenantId), this.tenantScope.getTenant()),
criteriaBuilder.equal(subQueryRoot.get(TenantUserEntity._userId), this.userScope.getUserId()),
criteriaBuilder.equal(subQueryRoot.get(TenantUserEntity._isActive), IsActive.Active)
);
} catch (InvalidApplicationException e) {
throw new RuntimeException(e);
}
}
)
))
)
));
query.multiselect(root.get(UserEntity._id).alias(UserEntity._id));
List<Tuple> results = this.entityManager.createQuery(query).getResultList();
if (results.isEmpty() && this.tenantScopeProperties.getAutoCreateTenantUser()) {
return this.createTenantUser();
} else {
return !results.isEmpty();
}
} finally {
if (usedResource) this.lockByKeyManager.unlock(lockId);
}
}
return false;
}
private boolean createTenantUser() throws InvalidApplicationException {
TenantUserEntity user = new TenantUserEntity();
user.setId(UUID.randomUUID());
user.setCreatedAt(Instant.now());
user.setUpdatedAt(Instant.now());
user.setIsActive(IsActive.Active);
user.setTenantId(this.tenantScope.getTenant());
user.setUserId(userScope.getUserId());
DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
definition.setName(UUID.randomUUID().toString());
definition.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);
definition.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
TransactionStatus status = null;
try {
status = transactionManager.getTransaction(definition);
this.entityManager.persist(user);
this.entityManager.flush();
transactionManager.commit(status);
} catch (Exception ex) {
if (status != null) transactionManager.rollback(status);
throw ex;
}
return true;
}
@Override
public void postHandle(@NonNull WebRequest request, ModelMap model) {
this.tenantScope.setTenant(null, null);
}
@Override
public void afterCompletion(@NonNull WebRequest request, Exception ex) {
}
}