change notification interceptor

This commit is contained in:
Efstratios Giannopoulos 2024-01-24 10:29:44 +02:00
parent d5292f5cec
commit c30f499f30
2 changed files with 10 additions and 224 deletions

View File

@ -4,21 +4,11 @@ package gr.cite.notification.web.interceptors;
import gr.cite.commons.web.oidc.principal.CurrentPrincipalResolver; import gr.cite.commons.web.oidc.principal.CurrentPrincipalResolver;
import gr.cite.commons.web.oidc.principal.extractor.ClaimExtractor; import gr.cite.commons.web.oidc.principal.extractor.ClaimExtractor;
import gr.cite.notification.common.JsonHandlingService; import gr.cite.notification.common.JsonHandlingService;
import gr.cite.notification.common.enums.ContactInfoType;
import gr.cite.notification.common.enums.IsActive;
import gr.cite.notification.common.lock.LockByKeyManager; import gr.cite.notification.common.lock.LockByKeyManager;
import gr.cite.notification.common.scope.user.UserScope; import gr.cite.notification.common.scope.user.UserScope;
import gr.cite.notification.common.types.user.AdditionalInfoEntity;
import gr.cite.notification.data.UserContactInfoEntity;
import gr.cite.notification.data.UserCredentialEntity; import gr.cite.notification.data.UserCredentialEntity;
import gr.cite.notification.data.UserEntity;
import gr.cite.notification.data.UserRoleEntity;
import gr.cite.notification.model.UserContactInfo;
import gr.cite.notification.model.UserCredential; import gr.cite.notification.model.UserCredential;
import gr.cite.notification.model.UserRole;
import gr.cite.notification.query.UserContactInfoQuery;
import gr.cite.notification.query.UserCredentialQuery; import gr.cite.notification.query.UserCredentialQuery;
import gr.cite.notification.query.UserRoleQuery;
import gr.cite.tools.data.query.QueryFactory; import gr.cite.tools.data.query.QueryFactory;
import gr.cite.tools.exception.MyForbiddenException; import gr.cite.tools.exception.MyForbiddenException;
import gr.cite.tools.fieldset.BaseFieldSet; import gr.cite.tools.fieldset.BaseFieldSet;
@ -28,21 +18,14 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.lang.NonNull; import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.transaction.PlatformTransactionManager; 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.ui.ModelMap;
import org.springframework.web.context.request.WebRequest; import org.springframework.web.context.request.WebRequest;
import org.springframework.web.context.request.WebRequestInterceptor; import org.springframework.web.context.request.WebRequestInterceptor;
import jakarta.persistence.EntityManager; import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext; import jakarta.persistence.PersistenceContext;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@Component @Component
public class UserInterceptor implements WebRequestInterceptor { public class UserInterceptor implements WebRequestInterceptor {
@ -79,214 +62,35 @@ public class UserInterceptor implements WebRequestInterceptor {
} }
@Override @Override
public void preHandle(WebRequest request) throws InterruptedException { public void preHandle(WebRequest request) {
UUID userId = null; UUID userId = null;
if (this.currentPrincipalResolver.currentPrincipal().isAuthenticated()) { if (this.currentPrincipalResolver.currentPrincipal().isAuthenticated()) {
String subjectId = this.claimExtractor.subjectString(this.currentPrincipalResolver.currentPrincipal()); String subjectId = this.claimExtractor.subjectString(this.currentPrincipalResolver.currentPrincipal());
if (subjectId == null || subjectId.isBlank()) throw new MyForbiddenException("Empty subjects not allowed"); if (subjectId == null || subjectId.isBlank()) throw new MyForbiddenException("Empty subjects not allowed");
UserInterceptorCacheService.UserInterceptorCacheValue cacheValue = this.userInterceptorCacheService.lookup(this.userInterceptorCacheService.buildKey(subjectId)); UserInterceptorCacheService.UserInterceptorCacheValue cacheValue = this.userInterceptorCacheService.lookup(this.userInterceptorCacheService.buildKey(subjectId));
if (cacheValue != null && emailExistsToUser(cacheValue.getEmails()) && userRolesSynced(cacheValue.getRoles())) { if (cacheValue != null) {
userId = cacheValue.getUserId(); userId = cacheValue.getUserId();
} else { } else {
boolean usedResource = false; userId = this.findExistingUserFromDbForce(subjectId);
try {
usedResource = this.lockByKeyManager.tryLock(subjectId, 5000, TimeUnit.MILLISECONDS);
String email = this.claimExtractor.email(this.currentPrincipalResolver.currentPrincipal());
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);
userId = this.findExistingUserFromDb(subjectId);
boolean isNewUser = userId == null;
if (isNewUser) {
UserEntity user = this.addNewUser(subjectId, email);
userId = user.getId();
}
if (!isNewUser) this.syncUserWithClaims(userId);
this.entityManager.flush();
transactionManager.commit(status);
} catch (Exception ex) {
if (status != null) transactionManager.rollback(status);
throw ex;
}
cacheValue = new UserInterceptorCacheService.UserInterceptorCacheValue(subjectId, userId);
cacheValue.setEmails(new ArrayList<>());
if (email != null && !email.isBlank()) cacheValue.getEmails().add(email);
cacheValue.setRoles(claimExtractor.roles(currentPrincipalResolver.currentPrincipal()));
this.userInterceptorCacheService.put(cacheValue);
} finally {
if (usedResource) this.lockByKeyManager.unlock(subjectId);
}
cacheValue = new UserInterceptorCacheService.UserInterceptorCacheValue(subjectId, userId);
this.userInterceptorCacheService.put(cacheValue);
} }
} }
this.userScope.setUserId(userId); this.userScope.setUserId(userId);
} }
private void syncUserWithClaims(UUID userId){ private UUID findExistingUserFromDbForce(String subjectId){
List<String> existingUserEmails = this.collectUserEmails(userId);
List<String> existingUserRoles = this.collectUserRoles(userId);
if (!this.emailExistsToUser(existingUserEmails)){
String email = this.claimExtractor.email(this.currentPrincipalResolver.currentPrincipal());
long contactUsedByOthersCount = this.queryFactory.query(UserContactInfoQuery.class).excludedUserIds(userId).type(ContactInfoType.Email).values(email).count();
if (contactUsedByOthersCount > 0) {
logger.warn("user contact exists to other user" + email);
} else {
Long emailContactsCount = this.queryFactory.query(UserContactInfoQuery.class).userIds(userId).type(ContactInfoType.Email).count();
UserContactInfoEntity contactInfo = this.buildEmailContact(userId, email);
contactInfo.setOrdinal(emailContactsCount.intValue());
this.entityManager.persist(contactInfo);
}
}
if (!this.userRolesSynced(existingUserRoles)){
this.syncRoles(userId);
}
}
private UUID findExistingUserFromDb(String subjectId){
UserCredentialEntity userCredential = this.queryFactory.query(UserCredentialQuery.class).externalIds(subjectId).firstAs(new BaseFieldSet().ensure(UserCredential._user)); UserCredentialEntity userCredential = this.queryFactory.query(UserCredentialQuery.class).externalIds(subjectId).firstAs(new BaseFieldSet().ensure(UserCredential._user));
if (userCredential != null) { if (userCredential != null) {
return userCredential.getUserId(); return userCredential.getUserId();
} else { } else {
String email = this.claimExtractor.email(this.currentPrincipalResolver.currentPrincipal()); throw new MyForbiddenException("User not created try again.");
if (email != null && !email.isBlank()) {
UserContactInfoEntity userContactInfo = this.queryFactory.query(UserContactInfoQuery.class).type(ContactInfoType.Email).values(email).firstAs(new BaseFieldSet().ensure(UserContactInfo._user));
if (userContactInfo != null) {
UserCredentialEntity credential = this.buildCredential(userContactInfo.getUserId(), subjectId);
this.entityManager.persist(credential);
return credential.getUserId();
}
} else {
throw new MyForbiddenException("Email is required");
}
}
return null;
}
private void syncRoles(UUID userId){
List<String> claimsRoles = claimExtractor.roles(currentPrincipalResolver.currentPrincipal());
if (claimsRoles == null) claimsRoles = new ArrayList<>();
claimsRoles = claimsRoles.stream().filter(x-> x != null && !x.isBlank()).distinct().collect(Collectors.toList());
List<UserRoleEntity> existingUserRoles = this.queryFactory.query(UserRoleQuery.class).userIds(userId).collect();
List<UUID> foundRoles = new ArrayList<>();
for (String claimRole : claimsRoles) {
UserRoleEntity roleEntity = existingUserRoles.stream().filter(x-> x.getRole().equals(claimRole)).findFirst().orElse(null);
if (roleEntity == null) {
roleEntity = this.buildRole(userId, claimRole);
this.entityManager.persist(roleEntity);
}
foundRoles.add(roleEntity.getId());
}
for (UserRoleEntity existing: existingUserRoles) {
if (!foundRoles.contains(existing.getId())){
this.entityManager.remove(existing);
}
} }
} }
private List<String> collectUserRoles(UUID userId){
List<UserRoleEntity> items = this.queryFactory.query(UserRoleQuery.class).userIds(userId).collectAs(new BaseFieldSet().ensure(UserRole._role));
return items == null ? new ArrayList<>() : items.stream().map(UserRoleEntity::getRole).collect(Collectors.toList());
}
private List<String> collectUserEmails(UUID userId){
List<UserContactInfoEntity> items = this.queryFactory.query(UserContactInfoQuery.class).userIds(userId).type(ContactInfoType.Email).collectAs(new BaseFieldSet().ensure(UserContactInfo._value));
return items == null ? new ArrayList<>() : items.stream().map(UserContactInfoEntity::getValue).collect(Collectors.toList());
}
private boolean emailExistsToUser(List<String> existingUserEmails){
String email = this.claimExtractor.email(this.currentPrincipalResolver.currentPrincipal());
return email == null || email.isBlank() ||
(existingUserEmails != null && existingUserEmails.stream().anyMatch(email::equals));
}
private boolean userRolesSynced(List<String> existingUserRoles){
List<String> claimsRoles = claimExtractor.roles(currentPrincipalResolver.currentPrincipal());
if (claimsRoles == null) claimsRoles = new ArrayList<>();
if (existingUserRoles == null) existingUserRoles = new ArrayList<>();
claimsRoles = claimsRoles.stream().filter(x-> x != null && !x.isBlank()).distinct().collect(Collectors.toList());
existingUserRoles = existingUserRoles.stream().filter(x-> x != null && !x.isBlank()).distinct().collect(Collectors.toList());
if (claimsRoles.size() != existingUserRoles.size()) return false;
for (String claim : claimsRoles ) {
if (existingUserRoles.stream().noneMatch(claim::equalsIgnoreCase)) return false;
}
return true;
}
private UserCredentialEntity buildCredential(UUID userId, String subjectId){
UserCredentialEntity data = new UserCredentialEntity();
data.setId(UUID.randomUUID());
data.setUserId(userId);
data.setCreatedAt(Instant.now());
data.setExternalId(subjectId);
return data;
}
private UserRoleEntity buildRole(UUID userId, String role){
UserRoleEntity data = new UserRoleEntity();
data.setId(UUID.randomUUID());
data.setUserId(userId);
data.setRole(role);
data.setCreatedAt(Instant.now());
return data;
}
private UserContactInfoEntity buildEmailContact(UUID userId, String email){
UserContactInfoEntity data = new UserContactInfoEntity();
data.setId(UUID.randomUUID());
data.setUserId(userId);
data.setValue(email);
data.setType(ContactInfoType.Email);
data.setOrdinal(0);
data.setCreatedAt(Instant.now());
return data;
}
private UserEntity addNewUser(String subjectId, String email){
List<String> roles = claimExtractor.roles(currentPrincipalResolver.currentPrincipal());
String name = this.claimExtractor.name(this.currentPrincipalResolver.currentPrincipal());
UserEntity user = new UserEntity();
user.setId(UUID.randomUUID());
user.setName(name);
user.setCreatedAt(Instant.now());
user.setUpdatedAt(Instant.now());
user.setIsActive(IsActive.Active);
user.setAdditionalInfo(this.jsonHandlingService.toJsonSafe(new AdditionalInfoEntity()));
this.entityManager.persist(user);
UserCredentialEntity credential = this.buildCredential(user.getId(), subjectId);
this.entityManager.persist(credential);
if (email != null && !email.isBlank()) {
UserContactInfoEntity contactInfo = this.buildEmailContact(user.getId(), email);
this.entityManager.persist(contactInfo);
}
if (roles != null) {
for (String role: roles) {
UserRoleEntity roleEntity = this.buildRole(user.getId(), role);
this.entityManager.persist(roleEntity);
}
}
return user;
}
@Override @Override
public void postHandle(@NonNull WebRequest request, ModelMap model) { public void postHandle(@NonNull WebRequest request, ModelMap model) {
this.userScope.setUserId(null); this.userScope.setUserId(null);

View File

@ -33,9 +33,6 @@ public class UserInterceptorCacheService extends CacheService<UserInterceptorCac
} }
private UUID userId; private UUID userId;
private List<String> roles;
private List<String> emails;
public UUID getUserId() { public UUID getUserId() {
return userId; return userId;
} }
@ -44,26 +41,11 @@ public class UserInterceptorCacheService extends CacheService<UserInterceptorCac
this.userId = userId; this.userId = userId;
} }
public List<String> getRoles() {
return roles;
}
public void setRoles(List<String> roles) {
this.roles = roles;
}
public List<String> getEmails() {
return emails;
}
public void setEmails(List<String> emails) {
this.emails = emails;
}
} }
@Autowired @Autowired
public UserInterceptorCacheService(UserInterceptorCacheOptions options, ConventionService conventionService) { public UserInterceptorCacheService(UserInterceptorCacheOptions options) {
super(options); super(options);
} }