add CredentialData

This commit is contained in:
Efstratios Giannopoulos 2024-01-16 12:26:29 +02:00
parent 04381aec20
commit d1cad30fcb
10 changed files with 136 additions and 49 deletions

View File

@ -0,0 +1,5 @@
package eu.eudat.authorization;
public class ClaimNames {
public static final String ExternalProviderName = "ExternalProviderName";
}

View File

@ -3,15 +3,15 @@ package eu.eudat.commons.types.usercredential;
import java.util.List;
public class UserCredentialDataEntity {
private List<String> providers;
private List<String> externalProviderNames;
private String email;
public List<String> getProviders() {
return providers;
public List<String> getExternalProviderNames() {
return externalProviderNames;
}
public void setProviders(List<String> providers) {
this.providers = providers;
public void setExternalProviderNames(List<String> externalProviderNames) {
this.externalProviderNames = externalProviderNames;
}
public String getEmail() {

View File

@ -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;

View File

@ -47,7 +47,7 @@ public class UserCredentialDataBuilder extends BaseBuilder<UserCredentialData, U
for (UserCredentialDataEntity d : data) {
UserCredentialData m = new UserCredentialData();
if (fields.hasField(this.asIndexer(UserCredentialData._email))) m.setEmail(d.getEmail());
if (fields.hasField(this.asIndexer(UserCredentialData._providers))) m.setProviders(d.getProviders());
if (fields.hasField(this.asIndexer(UserCredentialData._externalProviderNames))) m.setExternalProviderNames(d.getExternalProviderNames());
models.add(m);
}
this.logger.debug("build {} items", Optional.of(models).map(List::size).orElse(0));

View File

@ -3,17 +3,17 @@ package eu.eudat.model.usercredential;
import java.util.List;
public class UserCredentialData {
private List<String> providers;
public static final String _providers = "providers";
private List<String> externalProviderNames;
public static final String _externalProviderNames = "externalProviderNames";
private String email;
public static final String _email = "email";
public List<String> getProviders() {
return providers;
public List<String> getExternalProviderNames() {
return externalProviderNames;
}
public void setProviders(List<String> providers) {
this.providers = providers;
public void setExternalProviderNames(List<String> externalProviderNames) {
this.externalProviderNames = externalProviderNames;
}
public String getEmail() {

View File

@ -177,6 +177,8 @@ public class UserCredentialQuery extends QueryBase<UserCredentialEntity> {
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<UserCredentialEntity> {
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;
}

View File

@ -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 {
@ -86,13 +88,13 @@ public class UserInterceptor implements WebRequestInterceptor {
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();
}
this.entityManager.flush();
if (!isNewUser) this.syncUserWithClaims(userId);
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<String> existingUserEmails = this.collectUserEmails(userId);
List<String> 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<String> 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<String> getRolesFromClaims(){
List<String> 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<UserRoleEntity> existingUserRoles = this.queryFactory.query(UserRoleQuery.class).userIds(userId).collect();
List<UUID> 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<String> existingUserEmails){
String email = this.claimExtractor.email(this.currentPrincipalResolver.currentPrincipal());
private boolean containsPrincipalEmail(List<String> 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<String> principalCredentialProviders){
String provider = this.getProviderFromClaims();
return provider == null || provider.isBlank() ||
(principalCredentialProviders != null && principalCredentialProviders.stream().anyMatch(provider::equalsIgnoreCase));
}
private boolean userRolesSynced(List<String> existingUserRoles){
List<String> claimsRoles = claimExtractor.roles(currentPrincipalResolver.currentPrincipal());
if (claimsRoles == null) claimsRoles = new ArrayList<>();
List<String> 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;
@ -226,8 +275,28 @@ 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<String> roles = claimExtractor.roles(currentPrincipalResolver.currentPrincipal());
List<String> 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);

View File

@ -35,7 +35,8 @@ public class UserInterceptorCacheService extends CacheService<UserInterceptorCac
private String subjectId;
private UUID userId;
private List<String> roles;
private List<String> emails;
private String providerEmail;
private List<String> externalProviderNames;
public UUID getUserId() {
return userId;
@ -53,12 +54,20 @@ public class UserInterceptorCacheService extends CacheService<UserInterceptorCac
this.roles = roles;
}
public List<String> getEmails() {
return emails;
public String getProviderEmail() {
return providerEmail;
}
public void setEmails(List<String> emails) {
this.emails = emails;
public void setProviderEmail(String providerEmail) {
this.providerEmail = providerEmail;
}
public List<String> getExternalProviderNames() {
return externalProviderNames;
}
public void setExternalProviderNames(List<String> externalProviderNames) {
this.externalProviderNames = externalProviderNames;
}
}

View File

@ -37,3 +37,5 @@ idpclient:
- type: azp
Authorities:
- type: authorities
ExternalProviderName:
- type: identity_provider

View File

@ -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),