diff --git a/dmp-backend/core/src/main/java/eu/eudat/authorization/ClaimNames.java b/dmp-backend/core/src/main/java/eu/eudat/authorization/ClaimNames.java new file mode 100644 index 000000000..4e6edfdbd --- /dev/null +++ b/dmp-backend/core/src/main/java/eu/eudat/authorization/ClaimNames.java @@ -0,0 +1,5 @@ +package eu.eudat.authorization; + +public class ClaimNames { + public static final String ExternalProviderName = "ExternalProviderName"; +} diff --git a/dmp-backend/core/src/main/java/eu/eudat/commons/types/usercredential/UserCredentialDataEntity.java b/dmp-backend/core/src/main/java/eu/eudat/commons/types/usercredential/UserCredentialDataEntity.java index 22bdca050..2c69e0c9a 100644 --- a/dmp-backend/core/src/main/java/eu/eudat/commons/types/usercredential/UserCredentialDataEntity.java +++ b/dmp-backend/core/src/main/java/eu/eudat/commons/types/usercredential/UserCredentialDataEntity.java @@ -3,15 +3,15 @@ package eu.eudat.commons.types.usercredential; import java.util.List; public class UserCredentialDataEntity { - private List providers; + private List externalProviderNames; private String email; - public List getProviders() { - return providers; + public List getExternalProviderNames() { + return externalProviderNames; } - public void setProviders(List providers) { - this.providers = providers; + public void setExternalProviderNames(List externalProviderNames) { + this.externalProviderNames = externalProviderNames; } public String getEmail() { diff --git a/dmp-backend/core/src/main/java/eu/eudat/integrationevent/inbox/InboxRepositoryImpl.java b/dmp-backend/core/src/main/java/eu/eudat/integrationevent/inbox/InboxRepositoryImpl.java index d20305d24..d76431153 100644 --- a/dmp-backend/core/src/main/java/eu/eudat/integrationevent/inbox/InboxRepositoryImpl.java +++ b/dmp-backend/core/src/main/java/eu/eudat/integrationevent/inbox/InboxRepositoryImpl.java @@ -25,14 +25,12 @@ import org.springframework.context.ApplicationContext; import java.time.Instant; import java.util.List; -import java.util.Random; import java.util.UUID; import java.util.function.Function; public class InboxRepositoryImpl implements InboxRepository { protected final ApplicationContext applicationContext; - private final Random random = new Random(); private static final LoggerService logger = new LoggerService(LoggerFactory.getLogger(InboxRepositoryImpl.class)); private final JsonHandlingService jsonHandlingService; private final InboxProperties inboxProperties; @@ -85,18 +83,18 @@ public class InboxRepositoryImpl implements InboxRepository { transaction.commit(); } catch (OptimisticLockException ex) { // we get this if/when someone else already modified the notifications. We want to essentially ignore this, and keep working - this.logger.debug("Concurrency exception getting queue inbox. Skipping: {} ", ex.getMessage()); + logger.debug("Concurrency exception getting queue inbox. Skipping: {} ", ex.getMessage()); if (transaction != null) transaction.rollback(); candidate = null; } catch (Exception ex) { - this.logger.error("Problem getting list of queue inbox. Skipping: {}", ex.getMessage(), ex); + logger.error("Problem getting list of queue inbox. Skipping: {}", ex.getMessage(), ex); if (transaction != null) transaction.rollback(); candidate = null; } finally { if (entityManager != null) entityManager.close(); } } catch (Exception ex) { - this.logger.error("Problem getting list of queue inbox. Skipping: {}", ex.getMessage(), ex); + logger.error("Problem getting list of queue inbox. Skipping: {}", ex.getMessage(), ex); } return candidate; diff --git a/dmp-backend/core/src/main/java/eu/eudat/model/builder/usercredential/UserCredentialDataBuilder.java b/dmp-backend/core/src/main/java/eu/eudat/model/builder/usercredential/UserCredentialDataBuilder.java index 03fad3c98..87cd2e5dc 100644 --- a/dmp-backend/core/src/main/java/eu/eudat/model/builder/usercredential/UserCredentialDataBuilder.java +++ b/dmp-backend/core/src/main/java/eu/eudat/model/builder/usercredential/UserCredentialDataBuilder.java @@ -47,7 +47,7 @@ public class UserCredentialDataBuilder extends BaseBuilder providers; - public static final String _providers = "providers"; + private List externalProviderNames; + public static final String _externalProviderNames = "externalProviderNames"; private String email; public static final String _email = "email"; - public List getProviders() { - return providers; + public List getExternalProviderNames() { + return externalProviderNames; } - public void setProviders(List providers) { - this.providers = providers; + public void setExternalProviderNames(List externalProviderNames) { + this.externalProviderNames = externalProviderNames; } public String getEmail() { diff --git a/dmp-backend/core/src/main/java/eu/eudat/query/UserCredentialQuery.java b/dmp-backend/core/src/main/java/eu/eudat/query/UserCredentialQuery.java index e8284002f..d2cbc681a 100644 --- a/dmp-backend/core/src/main/java/eu/eudat/query/UserCredentialQuery.java +++ b/dmp-backend/core/src/main/java/eu/eudat/query/UserCredentialQuery.java @@ -177,6 +177,8 @@ public class UserCredentialQuery extends QueryBase { else if (item.prefix(UserCredential._user)) return UserCredentialEntity._userId; else if (item.match(UserCredential._user)) return UserCredentialEntity._userId; else if (item.match(UserCredential._createdAt) ) return UserCredentialEntity._createdAt; + else if (item.match(UserCredential._data) ) return UserCredentialEntity._data; + else if (item.prefix(UserCredential._data) ) return UserCredentialEntity._data; else return null; } @@ -187,6 +189,7 @@ public class UserCredentialQuery extends QueryBase { item.setExternalId(QueryBase.convertSafe(tuple, columns, UserCredentialEntity._externalId, String.class)); item.setUserId(QueryBase.convertSafe(tuple, columns, UserCredentialEntity._userId, UUID.class)); item.setCreatedAt(QueryBase.convertSafe(tuple, columns, UserCredentialEntity._createdAt, Instant.class)); + item.setData(QueryBase.convertSafe(tuple, columns, UserCredentialEntity._data, String.class)); return item; } diff --git a/dmp-backend/web/src/main/java/eu/eudat/interceptors/UserInterceptor.java b/dmp-backend/web/src/main/java/eu/eudat/interceptors/UserInterceptor.java index b81759a38..b3ce911b7 100644 --- a/dmp-backend/web/src/main/java/eu/eudat/interceptors/UserInterceptor.java +++ b/dmp-backend/web/src/main/java/eu/eudat/interceptors/UserInterceptor.java @@ -1,12 +1,14 @@ package eu.eudat.interceptors; +import eu.eudat.authorization.ClaimNames; import eu.eudat.commons.JsonHandlingService; import eu.eudat.commons.enums.ContactInfoType; import eu.eudat.commons.enums.IsActive; import eu.eudat.commons.lock.LockByKeyManager; import eu.eudat.commons.scope.user.UserScope; import eu.eudat.commons.types.user.AdditionalInfoEntity; +import eu.eudat.commons.types.usercredential.UserCredentialDataEntity; import eu.eudat.data.UserContactInfoEntity; import eu.eudat.data.UserCredentialEntity; import eu.eudat.data.UserEntity; @@ -14,7 +16,9 @@ import eu.eudat.data.UserRoleEntity; import eu.eudat.model.UserContactInfo; import eu.eudat.model.UserCredential; import eu.eudat.model.UserRole; -import eu.eudat.query.*; +import eu.eudat.query.UserContactInfoQuery; +import eu.eudat.query.UserCredentialQuery; +import eu.eudat.query.UserRoleQuery; import gr.cite.commons.web.oidc.principal.CurrentPrincipalResolver; import gr.cite.commons.web.oidc.principal.extractor.ClaimExtractor; import gr.cite.tools.data.query.QueryFactory; @@ -23,6 +27,7 @@ import gr.cite.tools.fieldset.BaseFieldSet; import gr.cite.tools.logging.LoggerService; import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceContext; +import org.apache.commons.validator.routines.EmailValidator; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.lang.NonNull; @@ -35,14 +40,11 @@ import org.springframework.ui.ModelMap; 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.ArrayList; import java.util.List; import java.util.UUID; -import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; -import java.util.concurrent.locks.ReentrantLock; @Component public class UserInterceptor implements WebRequestInterceptor { @@ -84,15 +86,15 @@ public class UserInterceptor implements WebRequestInterceptor { if (this.currentPrincipalResolver.currentPrincipal().isAuthenticated()) { String subjectId = this.claimExtractor.subjectString(this.currentPrincipalResolver.currentPrincipal()); if (subjectId == null || subjectId.isBlank()) throw new MyForbiddenException("Empty subjects not allowed"); - + UserInterceptorCacheService.UserInterceptorCacheValue cacheValue = this.userInterceptorCacheService.lookup(this.userInterceptorCacheService.buildKey(subjectId)); - if (cacheValue != null && emailExistsToUser(cacheValue.getEmails()) && userRolesSynced(cacheValue.getRoles())) { + if (cacheValue != null && emailExistsToPrincipal(cacheValue.getProviderEmail()) && userRolesSynced(cacheValue.getRoles()) && providerExistsToPrincipal(cacheValue.getExternalProviderNames())) { userId = cacheValue.getUserId(); } else { boolean usedResource = false; try { usedResource = this.lockByKeyManager.tryLock(subjectId, 5000, TimeUnit.MILLISECONDS); - String email = this.claimExtractor.email(this.currentPrincipalResolver.currentPrincipal()); + String email = this.getEmailFromClaims(); DefaultTransactionDefinition definition = new DefaultTransactionDefinition(); definition.setName(UUID.randomUUID().toString()); @@ -101,14 +103,18 @@ public class UserInterceptor implements WebRequestInterceptor { 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(); + + if (!isNewUser){ + this.syncUserWithClaims(userId, subjectId); + } this.entityManager.flush(); transactionManager.commit(status); @@ -118,9 +124,13 @@ public class UserInterceptor implements WebRequestInterceptor { } 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())); + cacheValue.setRoles(this.getRolesFromClaims()); + if (email != null && !email.isBlank()) cacheValue.setProviderEmail(email); + UserCredentialEntity userCredential = this.queryFactory.query(UserCredentialQuery.class).externalIds(subjectId).firstAs(new BaseFieldSet().ensure(UserCredential._data)); + if (userCredential != null && userCredential.getData() != null){ + UserCredentialDataEntity userCredentialDataEntity = this.jsonHandlingService.fromJsonSafe(UserCredentialDataEntity.class, userCredential.getData()); + if (userCredentialDataEntity != null) cacheValue.setExternalProviderNames(userCredentialDataEntity.getExternalProviderNames()); + } this.userInterceptorCacheService.put(cacheValue); } finally { @@ -132,11 +142,10 @@ public class UserInterceptor implements WebRequestInterceptor { this.userScope.setUserId(userId); } - private void syncUserWithClaims(UUID userId){ + private void syncUserWithClaims(UUID userId, String subjectId){ List existingUserEmails = this.collectUserEmails(userId); - List existingUserRoles = this.collectUserRoles(userId); - if (!this.emailExistsToUser(existingUserEmails)){ - String email = this.claimExtractor.email(this.currentPrincipalResolver.currentPrincipal()); + if (!this.containsPrincipalEmail(existingUserEmails)){ + String email = this.getEmailFromClaims(); long contactUsedByOthersCount = this.queryFactory.query(UserContactInfoQuery.class).excludedUserIds(userId).types(ContactInfoType.Email).values(email).count(); if (contactUsedByOthersCount > 0) { logger.warn("user contact exists to other user" + email); @@ -148,9 +157,37 @@ public class UserInterceptor implements WebRequestInterceptor { } } + List existingUserRoles = this.collectUserRoles(userId); if (!this.userRolesSynced(existingUserRoles)){ this.syncRoles(userId); } + + UserCredentialEntity userCredential = this.queryFactory.query(UserCredentialQuery.class).externalIds(subjectId).first(); + if (userCredential == null) { + throw new MyForbiddenException("UserCredential not found"); + } + else { + boolean updatedUserCredential = false; + UserCredentialDataEntity userCredentialDataEntity = this.jsonHandlingService.fromJsonSafe(UserCredentialDataEntity.class, userCredential.getData()); + if (userCredentialDataEntity == null) userCredentialDataEntity = new UserCredentialDataEntity(); + if (userCredentialDataEntity.getExternalProviderNames() == null) userCredentialDataEntity.setExternalProviderNames(new ArrayList<>()); + + String email = this.getEmailFromClaims(); + String provider = this.getProviderFromClaims(); + + if (email != null && !email.equalsIgnoreCase(userCredentialDataEntity.getEmail())) { + userCredentialDataEntity.setEmail(email); + updatedUserCredential = true; + } + if (provider != null && !provider.isBlank() && userCredentialDataEntity.getExternalProviderNames().stream().noneMatch(provider::equalsIgnoreCase)) { + userCredentialDataEntity.getExternalProviderNames().add(provider); + updatedUserCredential = true; + } + if (updatedUserCredential) { + userCredential.setData(this.jsonHandlingService.toJsonSafe(userCredentialDataEntity)); + this.entityManager.persist(userCredential); + } + } } private UUID findExistingUserFromDb(String subjectId){ @@ -158,7 +195,7 @@ public class UserInterceptor implements WebRequestInterceptor { if (userCredential != null) { return userCredential.getUserId(); } else { - String email = this.claimExtractor.email(this.currentPrincipalResolver.currentPrincipal()); + String email = this.getEmailFromClaims(); if (email != null && !email.isBlank()) { UserContactInfoEntity userContactInfo = this.queryFactory.query(UserContactInfoQuery.class).types(ContactInfoType.Email).values(email).firstAs(new BaseFieldSet().ensure(UserContactInfo._user)); if (userContactInfo != null) { @@ -174,14 +211,17 @@ public class UserInterceptor implements WebRequestInterceptor { return null; } - private void syncRoles(UUID userId){ + private List getRolesFromClaims(){ List claimsRoles = claimExtractor.roles(currentPrincipalResolver.currentPrincipal()); if (claimsRoles == null) claimsRoles = new ArrayList<>(); claimsRoles = claimsRoles.stream().filter(x-> x != null && !x.isBlank()).distinct().toList(); - + return claimsRoles; + } + + private void syncRoles(UUID userId){ List existingUserRoles = this.queryFactory.query(UserRoleQuery.class).userIds(userId).collect(); List foundRoles = new ArrayList<>(); - for (String claimRole : claimsRoles) { + for (String claimRole : this.getRolesFromClaims()) { UserRoleEntity roleEntity = existingUserRoles.stream().filter(x-> x.getRole().equals(claimRole)).findFirst().orElse(null); if (roleEntity == null) { roleEntity = this.buildRole(userId, claimRole); @@ -206,17 +246,26 @@ public class UserInterceptor implements WebRequestInterceptor { return items == null ? new ArrayList<>() : items.stream().map(UserContactInfoEntity::getValue).toList(); } - private boolean emailExistsToUser(List existingUserEmails){ - String email = this.claimExtractor.email(this.currentPrincipalResolver.currentPrincipal()); + private boolean containsPrincipalEmail(List existingUserEmails){ + String email = this.getEmailFromClaims(); return email == null || email.isBlank() || (existingUserEmails != null && existingUserEmails.stream().anyMatch(email::equals)); } + private boolean emailExistsToPrincipal(String existingUserEmail){ + String email = this.getEmailFromClaims(); + return email == null || email.isBlank() || email.equalsIgnoreCase(existingUserEmail); + } + + private boolean providerExistsToPrincipal(List principalCredentialProviders){ + String provider = this.getProviderFromClaims(); + return provider == null || provider.isBlank() || + (principalCredentialProviders != null && principalCredentialProviders.stream().anyMatch(provider::equalsIgnoreCase)); + } + private boolean userRolesSynced(List existingUserRoles){ - List claimsRoles = claimExtractor.roles(currentPrincipalResolver.currentPrincipal()); - if (claimsRoles == null) claimsRoles = new ArrayList<>(); + List claimsRoles = this.getRolesFromClaims(); if (existingUserRoles == null) existingUserRoles = new ArrayList<>(); - claimsRoles = claimsRoles.stream().filter(x-> x != null && !x.isBlank()).distinct().toList(); existingUserRoles = existingUserRoles.stream().filter(x-> x != null && !x.isBlank()).distinct().toList(); if (claimsRoles.size() != existingUserRoles.size()) return false; @@ -225,9 +274,29 @@ public class UserInterceptor implements WebRequestInterceptor { } return true; } + + private String getEmailFromClaims(){ + String email = this.claimExtractor.email(this.currentPrincipalResolver.currentPrincipal()); + if (email == null || email.isBlank() || !EmailValidator.getInstance().isValid(email)) return null; + return email.trim(); + } + + private String getProviderFromClaims(){ + String provider = this.claimExtractor.asString(this.currentPrincipalResolver.currentPrincipal(), ClaimNames.ExternalProviderName); + if (provider == null || provider.isBlank()) return null; + return provider.trim(); + } private UserCredentialEntity buildCredential(UUID userId, String subjectId){ UserCredentialEntity data = new UserCredentialEntity(); + UserCredentialDataEntity userCredentialDataEntity = new UserCredentialDataEntity(); + + String email = this.getEmailFromClaims(); + String provider = this.getProviderFromClaims(); + if (email != null && !email.isBlank()) userCredentialDataEntity.setEmail(email); + if (provider != null && !provider.isBlank()) userCredentialDataEntity.setExternalProviderNames(List.of(provider)); + data.setData(this.jsonHandlingService.toJsonSafe(userCredentialDataEntity)); + data.setId(UUID.randomUUID()); data.setUserId(userId); data.setCreatedAt(Instant.now()); @@ -258,7 +327,7 @@ public class UserInterceptor implements WebRequestInterceptor { private UserEntity addNewUser(String subjectId, String email){ - List roles = claimExtractor.roles(currentPrincipalResolver.currentPrincipal()); + List roles = this.getRolesFromClaims(); String name = this.claimExtractor.name(this.currentPrincipalResolver.currentPrincipal()); UserEntity user = new UserEntity(); @@ -267,7 +336,7 @@ public class UserInterceptor implements WebRequestInterceptor { user.setCreatedAt(Instant.now()); user.setUpdatedAt(Instant.now()); user.setIsActive(IsActive.Active); - user.setAdditionalInfo(this.jsonHandlingService.toJsonSafe(new AdditionalInfoEntity())); + user.setAdditionalInfo(this.jsonHandlingService.toJsonSafe(new AdditionalInfoEntity())); //TODO this.entityManager.persist(user); UserCredentialEntity credential = this.buildCredential(user.getId(), subjectId); diff --git a/dmp-backend/web/src/main/java/eu/eudat/interceptors/UserInterceptorCacheService.java b/dmp-backend/web/src/main/java/eu/eudat/interceptors/UserInterceptorCacheService.java index b3d80ec2d..f742041a8 100644 --- a/dmp-backend/web/src/main/java/eu/eudat/interceptors/UserInterceptorCacheService.java +++ b/dmp-backend/web/src/main/java/eu/eudat/interceptors/UserInterceptorCacheService.java @@ -35,7 +35,8 @@ public class UserInterceptorCacheService extends CacheService roles; - private List emails; + private String providerEmail; + private List externalProviderNames; public UUID getUserId() { return userId; @@ -53,12 +54,20 @@ public class UserInterceptorCacheService extends CacheService getEmails() { - return emails; + public String getProviderEmail() { + return providerEmail; } - public void setEmails(List emails) { - this.emails = emails; + public void setProviderEmail(String providerEmail) { + this.providerEmail = providerEmail; + } + + public List getExternalProviderNames() { + return externalProviderNames; + } + + public void setExternalProviderNames(List externalProviderNames) { + this.externalProviderNames = externalProviderNames; } } diff --git a/dmp-backend/web/src/main/resources/config/idpclaims.yml b/dmp-backend/web/src/main/resources/config/idpclaims.yml index 3372e4ca7..97ff4a10e 100644 --- a/dmp-backend/web/src/main/resources/config/idpclaims.yml +++ b/dmp-backend/web/src/main/resources/config/idpclaims.yml @@ -37,3 +37,5 @@ idpclient: - type: azp Authorities: - type: authorities + ExternalProviderName: + - type: identity_provider \ No newline at end of file diff --git a/dmp-db-scema/updates/00.01.018_addUserCredentialTable.sql b/dmp-db-scema/updates/00.01.018_addUserCredentialTable.sql index c7cb1d2d5..529b0d36e 100644 --- a/dmp-db-scema/updates/00.01.018_addUserCredentialTable.sql +++ b/dmp-db-scema/updates/00.01.018_addUserCredentialTable.sql @@ -8,6 +8,7 @@ BEGIN ( id uuid NOT NULL, "user" uuid NOT NULL, + "data" character varying NULL, external_id character varying(512) NOT NULL, created_at timestamp without time zone NOT NULL, PRIMARY KEY (id),