diff --git a/backend/core/src/main/java/org/opencdmp/authorization/AuthorizationProperties.java b/backend/core/src/main/java/org/opencdmp/authorization/AuthorizationProperties.java index 1e50fc423..d04203b69 100644 --- a/backend/core/src/main/java/org/opencdmp/authorization/AuthorizationProperties.java +++ b/backend/core/src/main/java/org/opencdmp/authorization/AuthorizationProperties.java @@ -10,6 +10,11 @@ public class AuthorizationProperties { private String globalAdminRole; private String tenantAdminRole; + private String globalUserRole; + private String tenantUserRole; + private Boolean autoAssignGlobalAdminToNewTenants; + private List allowedTenantRoles; + private List allowedGlobalRoles; public String getGlobalAdminRole() { return this.globalAdminRole; @@ -18,35 +23,6 @@ public class AuthorizationProperties { public void setGlobalAdminRole(String globalAdminRole) { this.globalAdminRole = globalAdminRole; } - private Boolean autoAssignGlobalAdminToNewTenants; - - public Boolean getAutoAssignGlobalAdminToNewTenants() { - return this.autoAssignGlobalAdminToNewTenants; - } - - public void setAutoAssignGlobalAdminToNewTenants(Boolean autoAssignGlobalAdminToNewTenants) { - this.autoAssignGlobalAdminToNewTenants = autoAssignGlobalAdminToNewTenants; - } - - private List allowedTenantRoles; - - public List getAllowedTenantRoles() { - return this.allowedTenantRoles; - } - - public void setAllowedTenantRoles(List allowedTenantRoles) { - this.allowedTenantRoles = allowedTenantRoles; - } - - private List allowedGlobalRoles; - - public List getAllowedGlobalRoles() { - return this.allowedGlobalRoles; - } - - public void setAllowedGlobalRoles(List allowedGlobalRoles) { - this.allowedGlobalRoles = allowedGlobalRoles; - } public String getTenantAdminRole() { return this.tenantAdminRole; @@ -55,4 +31,44 @@ public class AuthorizationProperties { public void setTenantAdminRole(String tenantAdminRole) { this.tenantAdminRole = tenantAdminRole; } + + public String getGlobalUserRole() { + return this.globalUserRole; + } + + public void setGlobalUserRole(String globalUserRole) { + this.globalUserRole = globalUserRole; + } + + public String getTenantUserRole() { + return this.tenantUserRole; + } + + public void setTenantUserRole(String tenantUserRole) { + this.tenantUserRole = tenantUserRole; + } + + public Boolean getAutoAssignGlobalAdminToNewTenants() { + return this.autoAssignGlobalAdminToNewTenants; + } + + public void setAutoAssignGlobalAdminToNewTenants(Boolean autoAssignGlobalAdminToNewTenants) { + this.autoAssignGlobalAdminToNewTenants = autoAssignGlobalAdminToNewTenants; + } + + public List getAllowedTenantRoles() { + return this.allowedTenantRoles; + } + + public void setAllowedTenantRoles(List allowedTenantRoles) { + this.allowedTenantRoles = allowedTenantRoles; + } + + public List getAllowedGlobalRoles() { + return this.allowedGlobalRoles; + } + + public void setAllowedGlobalRoles(List allowedGlobalRoles) { + this.allowedGlobalRoles = allowedGlobalRoles; + } } diff --git a/backend/core/src/main/java/org/opencdmp/commons/locale/LocaleConfiguration.java b/backend/core/src/main/java/org/opencdmp/commons/locale/LocaleConfiguration.java index 197a63ea8..3f3a2943c 100644 --- a/backend/core/src/main/java/org/opencdmp/commons/locale/LocaleConfiguration.java +++ b/backend/core/src/main/java/org/opencdmp/commons/locale/LocaleConfiguration.java @@ -15,6 +15,6 @@ public class LocaleConfiguration { } public LocaleProperties getProperties() { - return properties; + return this.properties; } } diff --git a/backend/core/src/main/java/org/opencdmp/commons/scope/tenant/MultitenancyProperties.java b/backend/core/src/main/java/org/opencdmp/commons/scope/tenant/MultitenancyProperties.java index ce18b6c6c..fd5530df0 100644 --- a/backend/core/src/main/java/org/opencdmp/commons/scope/tenant/MultitenancyProperties.java +++ b/backend/core/src/main/java/org/opencdmp/commons/scope/tenant/MultitenancyProperties.java @@ -8,15 +8,15 @@ public class MultitenancyProperties { private String defaultTenantCode; public boolean isMultitenant() { - return isMultitenant; + return this.isMultitenant; } public void setIsMultitenant(boolean multitenant) { - isMultitenant = multitenant; + this.isMultitenant = multitenant; } public String getDefaultTenantCode() { - return defaultTenantCode; + return this.defaultTenantCode; } public void setDefaultTenantCode(String defaultTenantCode) { diff --git a/backend/core/src/main/java/org/opencdmp/commons/scope/tenant/TenantScope.java b/backend/core/src/main/java/org/opencdmp/commons/scope/tenant/TenantScope.java index a70cb1470..20b9f0f10 100644 --- a/backend/core/src/main/java/org/opencdmp/commons/scope/tenant/TenantScope.java +++ b/backend/core/src/main/java/org/opencdmp/commons/scope/tenant/TenantScope.java @@ -1,14 +1,13 @@ package org.opencdmp.commons.scope.tenant; -import org.opencdmp.data.tenant.TenantScopedBaseEntity; import jakarta.persistence.EntityManager; import org.hibernate.Session; +import org.opencdmp.data.tenant.TenantScopedBaseEntity; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.web.context.annotation.RequestScope; import javax.management.InvalidApplicationException; - import java.util.UUID; import java.util.concurrent.atomic.AtomicReference; @@ -28,11 +27,11 @@ public class TenantScope { } public Boolean isMultitenant() { - return multitenancy.isMultitenant(); + return this.multitenancy.isMultitenant(); } public String getDefaultTenantCode() { - return multitenancy.getDefaultTenantCode(); + return this.multitenancy.getDefaultTenantCode(); } public Boolean isSet() { diff --git a/backend/core/src/main/java/org/opencdmp/model/deleter/UserCredentialDeleter.java b/backend/core/src/main/java/org/opencdmp/model/deleter/UserCredentialDeleter.java index e849daae9..dcef4d219 100644 --- a/backend/core/src/main/java/org/opencdmp/model/deleter/UserCredentialDeleter.java +++ b/backend/core/src/main/java/org/opencdmp/model/deleter/UserCredentialDeleter.java @@ -1,12 +1,13 @@ package org.opencdmp.model.deleter; -import org.opencdmp.data.TenantEntityManager; -import org.opencdmp.data.UserCredentialEntity; -import org.opencdmp.query.UserCredentialQuery; import gr.cite.tools.data.deleter.Deleter; import gr.cite.tools.data.query.QueryFactory; import gr.cite.tools.logging.LoggerService; import gr.cite.tools.logging.MapLogEntry; +import org.opencdmp.data.TenantEntityManager; +import org.opencdmp.data.UserCredentialEntity; +import org.opencdmp.query.UserCredentialQuery; +import org.opencdmp.service.keycloak.KeycloakService; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.config.ConfigurableBeanFactory; @@ -19,22 +20,24 @@ import java.util.Optional; import java.util.UUID; @Component -@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE) +@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) public class UserCredentialDeleter implements Deleter { private static final LoggerService logger = new LoggerService(LoggerFactory.getLogger(UserCredentialDeleter.class)); private final TenantEntityManager entityManager; protected final QueryFactory queryFactory; + private final KeycloakService keycloakService; @Autowired public UserCredentialDeleter( - TenantEntityManager entityManager, - QueryFactory queryFactory + TenantEntityManager entityManager, + QueryFactory queryFactory, KeycloakService keycloakService ) { this.entityManager = entityManager; this.queryFactory = queryFactory; + this.keycloakService = keycloakService; } public void deleteAndSaveByIds(List ids) throws InvalidApplicationException { @@ -62,6 +65,8 @@ public class UserCredentialDeleter implements Deleter { logger.trace("deleting item"); this.entityManager.remove(item); logger.trace("deleted item"); + + this.keycloakService.removeFromAllGroups(item.getExternalId()); } } diff --git a/backend/core/src/main/java/org/opencdmp/model/persist/actionconfirmation/RemoveCredentialRequestPersist.java b/backend/core/src/main/java/org/opencdmp/model/persist/actionconfirmation/RemoveCredentialRequestPersist.java index 14d03e92b..9cea87404 100644 --- a/backend/core/src/main/java/org/opencdmp/model/persist/actionconfirmation/RemoveCredentialRequestPersist.java +++ b/backend/core/src/main/java/org/opencdmp/model/persist/actionconfirmation/RemoveCredentialRequestPersist.java @@ -1,7 +1,7 @@ package org.opencdmp.model.persist.actionconfirmation; -import org.opencdmp.commons.validation.BaseValidator; import gr.cite.tools.validation.specification.Specification; +import org.opencdmp.commons.validation.BaseValidator; import org.opencdmp.convention.ConventionService; import org.opencdmp.errorcode.ErrorThesaurusProperties; import org.springframework.beans.factory.config.ConfigurableBeanFactory; @@ -20,7 +20,7 @@ public class RemoveCredentialRequestPersist { public static final String _credentialId = "credentialId"; public UUID getCredentialId() { - return credentialId; + return this.credentialId; } public void setCredentialId(UUID credentialId) { @@ -51,7 +51,7 @@ public class RemoveCredentialRequestPersist { return Arrays.asList( this.spec() .must(() -> this.isValidGuid(item.getCredentialId())) - .failOn(RemoveCredentialRequestPersist._credentialId).failWith(messageSource.getMessage("Validation_Required", new Object[]{RemoveCredentialRequestPersist._credentialId}, LocaleContextHolder.getLocale())) + .failOn(RemoveCredentialRequestPersist._credentialId).failWith(this.messageSource.getMessage("Validation_Required", new Object[]{RemoveCredentialRequestPersist._credentialId}, LocaleContextHolder.getLocale())) ); } } diff --git a/backend/core/src/main/java/org/opencdmp/service/keycloak/KeycloakService.java b/backend/core/src/main/java/org/opencdmp/service/keycloak/KeycloakService.java index ff9cd9934..43373eb02 100644 --- a/backend/core/src/main/java/org/opencdmp/service/keycloak/KeycloakService.java +++ b/backend/core/src/main/java/org/opencdmp/service/keycloak/KeycloakService.java @@ -1,10 +1,8 @@ package org.opencdmp.service.keycloak; import org.jetbrains.annotations.NotNull; -import org.keycloak.representations.idm.GroupRepresentation; -import java.util.HashMap; -import java.util.UUID; +import java.util.List; public interface KeycloakService { @@ -12,6 +10,10 @@ public interface KeycloakService { void removeUserFromGroup(@NotNull String subjectId, String groupId); + List getUserGroups(String subjectId); + + void removeFromAllGroups(String subjectId); + void addUserToGlobalRoleGroup(String subjectId, String role); void removeUserGlobalRoleGroup(@NotNull String subjectId, String role); diff --git a/backend/core/src/main/java/org/opencdmp/service/keycloak/KeycloakServiceImpl.java b/backend/core/src/main/java/org/opencdmp/service/keycloak/KeycloakServiceImpl.java index e732c321c..f71c66311 100644 --- a/backend/core/src/main/java/org/opencdmp/service/keycloak/KeycloakServiceImpl.java +++ b/backend/core/src/main/java/org/opencdmp/service/keycloak/KeycloakServiceImpl.java @@ -6,6 +6,7 @@ import org.opencdmp.convention.ConventionService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -33,7 +34,23 @@ public class KeycloakServiceImpl implements KeycloakService { public void removeUserFromGroup(@NotNull String subjectId, String groupId) { this.api.users().removeUserFromGroup(subjectId, groupId); } + + @Override + public List getUserGroups(String subjectId) { + if (this.configuration.getProperties().getAuthorities() == null) return new ArrayList<>(); + List group = this.api.users().getGroups(subjectId); + if (group != null) return group.stream().map(GroupRepresentation::getId).toList(); + return new ArrayList<>(); + } + @Override + public void removeFromAllGroups(String subjectId){ + List existingGroups = this.getUserGroups(subjectId); + for (String existingGroup : existingGroups){ + this.removeUserFromGroup(subjectId, existingGroup); + } + } + @Override public void addUserToGlobalRoleGroup(String subjectId, String role) { if (this.configuration.getProperties().getAuthorities() == null) return; diff --git a/backend/core/src/main/java/org/opencdmp/service/user/UserService.java b/backend/core/src/main/java/org/opencdmp/service/user/UserService.java index 458de6b83..8c4947890 100644 --- a/backend/core/src/main/java/org/opencdmp/service/user/UserService.java +++ b/backend/core/src/main/java/org/opencdmp/service/user/UserService.java @@ -1,17 +1,17 @@ package org.opencdmp.service.user; import com.fasterxml.jackson.core.JsonProcessingException; -import org.opencdmp.model.user.User; -import org.opencdmp.model.persist.actionconfirmation.RemoveCredentialRequestPersist; -import org.opencdmp.model.persist.UserMergeRequestPersist; -import org.opencdmp.model.persist.UserPersist; -import org.opencdmp.model.persist.UserRolePatchPersist; import gr.cite.tools.exception.MyApplicationException; import gr.cite.tools.exception.MyForbiddenException; import gr.cite.tools.exception.MyNotFoundException; import gr.cite.tools.exception.MyValidationException; import gr.cite.tools.fieldset.FieldSet; import jakarta.xml.bind.JAXBException; +import org.opencdmp.model.persist.UserMergeRequestPersist; +import org.opencdmp.model.persist.UserPersist; +import org.opencdmp.model.persist.UserRolePatchPersist; +import org.opencdmp.model.persist.actionconfirmation.RemoveCredentialRequestPersist; +import org.opencdmp.model.user.User; import javax.management.InvalidApplicationException; import java.io.IOException; @@ -40,5 +40,4 @@ public interface UserService { void confirmMergeAccount(String token) throws InvalidApplicationException, IOException; void confirmRemoveCredential(String token) throws InvalidApplicationException; - } diff --git a/backend/core/src/main/java/org/opencdmp/service/user/UserServiceImpl.java b/backend/core/src/main/java/org/opencdmp/service/user/UserServiceImpl.java index c3139f5f4..92a860e7a 100644 --- a/backend/core/src/main/java/org/opencdmp/service/user/UserServiceImpl.java +++ b/backend/core/src/main/java/org/opencdmp/service/user/UserServiceImpl.java @@ -39,6 +39,7 @@ import org.opencdmp.commons.types.user.AdditionalInfoEntity; import org.opencdmp.commons.types.usercredential.UserCredentialDataEntity; import org.opencdmp.convention.ConventionService; import org.opencdmp.data.*; +import org.opencdmp.data.tenant.TenantScopedBaseEntity; import org.opencdmp.errorcode.ErrorThesaurusProperties; import org.opencdmp.event.EventBroker; import org.opencdmp.event.UserTouchedEvent; @@ -74,6 +75,7 @@ import java.io.PrintWriter; import java.time.Instant; import java.util.ArrayList; import java.util.List; +import java.util.Objects; import java.util.UUID; import java.util.stream.Collectors; @@ -100,14 +102,11 @@ public class UserServiceImpl implements UserService { private final XmlHandlingService xmlHandlingService; private final QueryFactory queryFactory; private final UserScope userScope; - private final KeycloakService keycloakService; private final ActionConfirmationService actionConfirmationService; private final NotificationProperties notificationProperties; private final NotifyIntegrationEventHandler eventHandler; - private final ValidatorFactory validatorFactory; - private final ElasticService elasticService; private final UserTouchedIntegrationEventHandler userTouchedIntegrationEventHandler; private final UserRemovalIntegrationEventHandler userRemovalIntegrationEventHandler; @@ -193,7 +192,7 @@ public class UserServiceImpl implements UserService { if (persist == null) return data; if (persist.getOrganization() != null) { ReferenceEntity organization = this.buildReferenceEntity(persist.getOrganization()); - data.setOrganizationId(organization != null ? organization.getId() : null); + data.setOrganizationId(organization.getId()); } data.setRoleOrganization(persist.getRoleOrganization()); data.setCulture(persist.getCulture()); @@ -205,7 +204,7 @@ public class UserServiceImpl implements UserService { private @NotNull ReferenceEntity buildReferenceEntity(ReferencePersist model) throws InvalidApplicationException { - ReferenceEntity referenceEntity = null; + ReferenceEntity referenceEntity; if (this.conventionService.isValidGuid(model.getId())) { referenceEntity = this.entityManager.find(ReferenceEntity.class, model.getId()); if (referenceEntity == null) @@ -333,6 +332,8 @@ public class UserServiceImpl implements UserService { this.eventBroker.emit(new UserTouchedEvent(data.getId())); + this.syncKeycloakRoles(data.getId()); + this.userTouchedIntegrationEventHandler.handle(data.getId()); return this.builderFactory.builder(UserBuilder.class).authorize(AuthorizationFlags.OwnerOrDmpAssociatedOrPermission).build(BaseFieldSet.build(fields, User._id), data); } @@ -352,7 +353,6 @@ public class UserServiceImpl implements UserService { item.setRole(roleName); item.setCreatedAt(Instant.now()); this.entityManager.persist(item); - this.keycloakService.addUserToGlobalRoleGroup(subjectId, roleName); } foundIds.add(item.getId()); } @@ -360,7 +360,6 @@ public class UserServiceImpl implements UserService { this.entityManager.flush(); List toDelete = existingItems.stream().filter(x -> foundIds.stream().noneMatch(y -> y.equals(x.getId()))).collect(Collectors.toList()); - toDelete.forEach(x -> this.keycloakService.removeUserGlobalRoleGroup(subjectId, x.getRole())); this.deleterFactory.deleter(UserRoleDeleter.class).deleteAndSave(toDelete); this.entityManager.flush(); @@ -388,7 +387,6 @@ public class UserServiceImpl implements UserService { item.setCreatedAt(Instant.now()); item.setTenantId(this.tenantScope.getTenant()); this.entityManager.persist(item); - this.keycloakService.addUserToTenantRoleGroup(subjectId, this.tenantScope.getTenantCode(), roleName); } foundIds.add(item.getId()); } @@ -396,13 +394,6 @@ public class UserServiceImpl implements UserService { this.entityManager.flush(); List toDelete = existingItems.stream().filter(x-> foundIds.stream().noneMatch(y-> y.equals(x.getId()))).collect(Collectors.toList()); - toDelete.forEach(x -> { - try { - this.keycloakService.removeUserTenantRoleGroup(subjectId, this.tenantScope.getTenantCode(), x.getRole()); - } catch (InvalidApplicationException e) { - throw new RuntimeException(e); - } - }); this.deleterFactory.deleter(UserRoleDeleter.class).deleteAndSave(toDelete); this.entityManager.flush(); @@ -506,7 +497,6 @@ public class UserServiceImpl implements UserService { if (this.userScope.getUserIdSafe() == null) throw new MyForbiddenException(this.errors.getForbidden().getCode(), this.errors.getForbidden().getMessage()); - String token = this.createMergeAccountConfirmation(model.getEmail()); this.createMergeNotificationEvent(token, user, model.getEmail(), NotificationContactType.EMAIL); } @@ -571,8 +561,6 @@ public class UserServiceImpl implements UserService { return persist.getToken(); } - - private String createRemoveConfirmation(UUID credentialId) throws JAXBException, InvalidApplicationException { ActionConfirmationPersist persist = new ActionConfirmationPersist(); persist.setType(ActionConfirmationType.RemoveCredential); @@ -602,7 +590,7 @@ public class UserServiceImpl implements UserService { this.checkActionState(action); MergeAccountConfirmationEntity mergeAccountConfirmationEntity = this.xmlHandlingService.fromXmlSafe(MergeAccountConfirmationEntity.class, action.getData()); - if (mergeAccountConfirmationEntity == null) throw new MyNotFoundException(this.messageSource.getMessage("General_ItemNotFound", new Object[]{mergeAccountConfirmationEntity, MergeAccountConfirmationEntity.class.getSimpleName()}, LocaleContextHolder.getLocale())); + if (mergeAccountConfirmationEntity == null) throw new MyNotFoundException(this.messageSource.getMessage("General_ItemNotFound", new Object[]{action.getId(), MergeAccountConfirmationEntity.class.getSimpleName()}, LocaleContextHolder.getLocale())); UserContactInfoEntity userContactInfoEntity = this.queryFactory.query(UserContactInfoQuery.class).values(mergeAccountConfirmationEntity.getEmail()).types(ContactInfoType.Email).first(); if (userContactInfoEntity == null) throw new MyNotFoundException(this.messageSource.getMessage("General_ItemNotFound", new Object[]{mergeAccountConfirmationEntity.getEmail(), User.class.getSimpleName()}, LocaleContextHolder.getLocale())); @@ -627,6 +615,29 @@ public class UserServiceImpl implements UserService { this.userTouchedIntegrationEventHandler.handle(newUser.getId()); this.userRemovalIntegrationEventHandler.handle(userToBeMerge.getId()); + + if (!newUser.getId().equals(userToBeMerge.getId())) { + this.syncKeycloakRoles(newUser.getId()); + } + + } + + private void syncKeycloakRoles(UUID userId){ + List userCredentials = this.queryFactory.query(UserCredentialQuery.class).disableTracking().userIds(userId).collect(); + List userRoles = this.queryFactory.query(UserRoleQuery.class).disableTracking().userIds(userId).collect(); + List tenants = this.queryFactory.query(TenantQuery.class).disableTracking().ids(userRoles.stream().map(TenantScopedBaseEntity::getTenantId).filter(Objects::nonNull).toList()).collect(); + + for (UserCredentialEntity userCredential : userCredentials){ + this.keycloakService.removeFromAllGroups(userCredential.getExternalId()); + for (UserRoleEntity userRole : userRoles) { + if (this.authorizationProperties.getAllowedGlobalRoles().contains(userRole.getRole())){ + this.keycloakService.addUserToGlobalRoleGroup(userCredential.getExternalId(), userRole.getRole()); + } else if (this.authorizationProperties.getAllowedTenantRoles().contains(userRole.getRole())){ + String tenantCode = userRole.getTenantId() == null ? this.tenantScope.getDefaultTenantCode() : tenants.stream().filter(x-> x.getId().equals(userRole.getTenantId())).map(TenantEntity::getCode).findFirst().orElse(null); + if (!this.conventionService.isNullOrEmpty(tenantCode)) this.keycloakService.addUserToTenantRoleGroup(userCredential.getExternalId(), tenantCode, userRole.getRole()); + } + } + } } private void mergeNewUserToOld(UserEntity newUser, UserEntity oldUser) throws IOException, InvalidApplicationException { @@ -731,8 +742,6 @@ public class UserServiceImpl implements UserService { } } - - public void confirmRemoveCredential(String token) throws InvalidApplicationException { ActionConfirmationEntity action = this.queryFactory.query(ActionConfirmationQuery.class).tokens(token).types(ActionConfirmationType.RemoveCredential).isActive(IsActive.Active).first(); if (action == null) throw new MyNotFoundException(this.messageSource.getMessage("General_ItemNotFound", new Object[]{token, ActionConfirmationEntity.class.getSimpleName()}, LocaleContextHolder.getLocale())); @@ -740,12 +749,12 @@ public class UserServiceImpl implements UserService { this.checkActionState(action); RemoveCredentialRequestEntity removeCredentialRequestEntity = this.xmlHandlingService.fromXmlSafe(RemoveCredentialRequestEntity.class, action.getData()); - if (removeCredentialRequestEntity == null) throw new MyNotFoundException(this.messageSource.getMessage("General_ItemNotFound", new Object[]{removeCredentialRequestEntity, RemoveCredentialRequestEntity.class.getSimpleName()}, LocaleContextHolder.getLocale())); + if (removeCredentialRequestEntity == null) throw new MyNotFoundException(this.messageSource.getMessage("General_ItemNotFound", new Object[]{action.getId(), RemoveCredentialRequestEntity.class.getSimpleName()}, LocaleContextHolder.getLocale())); UserCredentialEntity userCredentialEntity = this.queryFactory.query(UserCredentialQuery.class).ids(removeCredentialRequestEntity.getCredentialId()).first(); if (userCredentialEntity == null) throw new MyNotFoundException(this.messageSource.getMessage("General_ItemNotFound", new Object[]{removeCredentialRequestEntity.getCredentialId(), UserCredential.class.getSimpleName()}, LocaleContextHolder.getLocale())); - if (!this.userScope.getUserIdSafe().equals(userCredentialEntity.getId())) throw new MyForbiddenException("Only requested user can approve"); + if (!this.userScope.getUserIdSafe().equals(userCredentialEntity.getUserId())) throw new MyForbiddenException("Only requested user can approve"); if (userCredentialEntity.getData() != null){ UserCredentialDataEntity userCredentialDataEntity = this.jsonHandlingService.fromJsonSafe(UserCredentialDataEntity.class, userCredentialEntity.getData()); @@ -763,8 +772,16 @@ public class UserServiceImpl implements UserService { this.entityManager.flush(); + this.keycloakService.removeFromAllGroups(userCredentialEntity.getExternalId()); + this.addToDefaultUserGroups(userCredentialEntity.getExternalId()); + this.userTouchedIntegrationEventHandler.handle(userCredentialEntity.getUserId()); } + + private void addToDefaultUserGroups(String subjectId){ + this.keycloakService.addUserToGlobalRoleGroup(subjectId, this.authorizationProperties.getGlobalUserRole()); + this.keycloakService.addUserToTenantRoleGroup(subjectId, this.tenantScope.getDefaultTenantCode(), this.authorizationProperties.getTenantUserRole()); + } private void checkActionState(ActionConfirmationEntity action) throws MyApplicationException { if (action.getStatus().equals(ActionConfirmationStatus.Accepted)){ diff --git a/backend/web/src/main/resources/config/authorization.yml b/backend/web/src/main/resources/config/authorization.yml index 66baec614..bcc2e9e42 100644 --- a/backend/web/src/main/resources/config/authorization.yml +++ b/backend/web/src/main/resources/config/authorization.yml @@ -1,6 +1,8 @@ authorization: globalAdminRole: Admin tenantAdminRole: TenantAdmin + globalUserRole: User + tenantUserRole: TenantUser autoAssignGlobalAdminToNewTenants: true allowedTenantRoles: - TenantAdmin diff --git a/dmp-frontend/src/app/core/services/user/user.service.ts b/dmp-frontend/src/app/core/services/user/user.service.ts index dd0e34ea1..4da784898 100644 --- a/dmp-frontend/src/app/core/services/user/user.service.ts +++ b/dmp-frontend/src/app/core/services/user/user.service.ts @@ -106,11 +106,11 @@ export class UserService { .post(url, item).pipe( catchError((error: any) => throwError(error))); } - + removeCredentialAccount(item: RemoveCredentialRequestPersist): Observable { const url = `${this.apiBase}/mine/remove-credential-request`; - console.log(item); - console.log(url); + console.log(item); + console.log(url); return this.http .post(url, item).pipe( @@ -125,6 +125,14 @@ export class UserService { catchError((error: any) => throwError(error))); } + confirmRemoveCredentialAccount(token: Guid): Observable { + const url = `${this.apiBase}/mine/confirm-remove-credential/token/${token}`; + + return this.http + .get(url).pipe( + catchError((error: any) => throwError(error))); + } + // // Autocomplete Commons // diff --git a/dmp-frontend/src/app/ui/auth/login/merge-email-confirmation/merge-email-confirmation.component.ts b/dmp-frontend/src/app/ui/auth/login/merge-email-confirmation/merge-email-confirmation.component.ts index 490e0a772..f0eb5e887 100644 --- a/dmp-frontend/src/app/ui/auth/login/merge-email-confirmation/merge-email-confirmation.component.ts +++ b/dmp-frontend/src/app/ui/auth/login/merge-email-confirmation/merge-email-confirmation.component.ts @@ -1,11 +1,7 @@ import { Component, OnInit } from "@angular/core"; -import { UntypedFormControl } from '@angular/forms'; -import { MatDialog } from "@angular/material/dialog"; import { ActivatedRoute, Router } from "@angular/router"; -import { AuthService } from "@app/core/services/auth/auth.service"; import { SnackBarNotificationLevel, UiNotificationService } from '@app/core/services/notification/ui-notification-service'; import { UserService } from "@app/core/services/user/user.service"; -import { PopupNotificationDialogComponent } from "@app/library/notification/popup/popup-notification.component"; import { BaseComponent } from '@common/base/base.component'; import { Guid } from "@common/types/guid"; import { TranslateService } from '@ngx-translate/core'; @@ -17,20 +13,14 @@ import { takeUntil } from "rxjs/operators"; styleUrls: ['./merge-email-confirmation.component.scss'] }) export class MergeEmailConfirmation extends BaseComponent implements OnInit { - - private token: Guid; - public emailFormControl = new UntypedFormControl(''); - public mailSent: boolean = false; - + private token: Guid; + get showForm(): boolean { return this.token != null; } - + constructor( - //TODO: refactor - // private emailConfirmationService: MergeEmailConfirmationService, - // private authService: AuthService, private userService: UserService, private route: ActivatedRoute, private router: Router, @@ -45,30 +35,12 @@ export class MergeEmailConfirmation extends BaseComponent implements OnInit { const token = params['token'] if (token != null) { this.token = token; - // this.showForm = false; - //TODO: refactor - // this.emailConfirmationService.emailConfirmation(token) - // .pipe(takeUntil(this._destroyed)) - // .subscribe( - // result => { - // const email = this.authService.getUserProfileEmail(); - // if(!email || !result || (email == result)) - // this.authService.clear(); - // this.uiNotificationService.snackBarNotification(this.language.instant('USER-PROFILE.MERGING-SUCCESS'), SnackBarNotificationLevel.Success); - // this.onCallbackEmailConfirmationSuccess(); - // }, - // error => this.onCallbackError(error) - // ) - } else { - // this.showForm = true; } }); } onConfirm(): void { - console.log('onConfirm'); if (this.showForm === false) return; - console.log('active'); this.userService.confirmMergeAccount(this.token) .subscribe(result => { @@ -80,7 +52,10 @@ export class MergeEmailConfirmation extends BaseComponent implements OnInit { } onCallbackEmailConfirmationSuccess() { - this.router.navigate(['home']); + this.router.navigate(['home']) + .then(() => { + window.location.reload(); + }); } onCallbackError(error: any) { diff --git a/dmp-frontend/src/app/ui/auth/login/unlink-email-confirmation/unlink-email-confirmation.component.html b/dmp-frontend/src/app/ui/auth/login/unlink-email-confirmation/unlink-email-confirmation.component.html index 8b1378917..35483afd9 100644 --- a/dmp-frontend/src/app/ui/auth/login/unlink-email-confirmation/unlink-email-confirmation.component.html +++ b/dmp-frontend/src/app/ui/auth/login/unlink-email-confirmation/unlink-email-confirmation.component.html @@ -1 +1,28 @@ + diff --git a/dmp-frontend/src/app/ui/auth/login/unlink-email-confirmation/unlink-email-confirmation.component.scss b/dmp-frontend/src/app/ui/auth/login/unlink-email-confirmation/unlink-email-confirmation.component.scss index e69de29bb..20bcbbf90 100644 --- a/dmp-frontend/src/app/ui/auth/login/unlink-email-confirmation/unlink-email-confirmation.component.scss +++ b/dmp-frontend/src/app/ui/auth/login/unlink-email-confirmation/unlink-email-confirmation.component.scss @@ -0,0 +1,19 @@ +.unlink-account { + height: fit-content; + //margin-top: 80px; + min-height: 100vh; + background-color: #ffffff; +} + +.unlink-account-title { + font-size: 1.25rem; + color: #212121; + padding-top: 4.1875rem; + padding-left: 3.75rem; + padding-bottom: 4.0625rem; +} + +.unlink-account-content { + margin-left: 9rem; + margin-right: 11rem; +} diff --git a/dmp-frontend/src/app/ui/auth/login/unlink-email-confirmation/unlink-email-confirmation.component.ts b/dmp-frontend/src/app/ui/auth/login/unlink-email-confirmation/unlink-email-confirmation.component.ts index 339d3e631..083438d52 100644 --- a/dmp-frontend/src/app/ui/auth/login/unlink-email-confirmation/unlink-email-confirmation.component.ts +++ b/dmp-frontend/src/app/ui/auth/login/unlink-email-confirmation/unlink-email-confirmation.component.ts @@ -1,7 +1,9 @@ import { Component, OnInit } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { SnackBarNotificationLevel, UiNotificationService } from '@app/core/services/notification/ui-notification-service'; +import { UserService } from '@app/core/services/user/user.service'; import { BaseComponent } from '@common/base/base.component'; +import { Guid } from '@common/types/guid'; import { TranslateService } from '@ngx-translate/core'; import { takeUntil } from "rxjs/operators"; @@ -10,32 +12,45 @@ import { takeUntil } from "rxjs/operators"; templateUrl: './unlink-email-confirmation.component.html' }) export class UnlinkEmailConfirmation extends BaseComponent implements OnInit { + private token: Guid; - constructor( + + get showForm(): boolean { + return this.token != null; + } + + constructor( + private userService: UserService, private route: ActivatedRoute, private router: Router, private language: TranslateService, - private uiNotificationService: UiNotificationService + private uiNotificationService: UiNotificationService, ) { super(); } - ngOnInit(): void { - this.route.params + ngOnInit() { + this.route.params .pipe(takeUntil(this._destroyed)) .subscribe(params => { const token = params['token'] if (token != null) { - //TODO refactor - // this.emailConfirmationService.emailConfirmation(token) - // .pipe(takeUntil(this._destroyed)) - // .subscribe( - // result => this.onCallbackEmailConfirmationSuccess(), - // error => this.onCallbackError(error) - // ) + this.token = token; } }); - } + } - onCallbackEmailConfirmationSuccess() { + onConfirm(): void { + if (this.showForm === false) return; + + this.userService.confirmRemoveCredentialAccount(this.token) + .subscribe(result => { + if (result) { + this.onCallbackConfirmationSuccess(); + } + }, + error => this.onCallbackError(error)); + } + + onCallbackConfirmationSuccess() { this.router.navigate(['home']); } @@ -43,10 +58,8 @@ export class UnlinkEmailConfirmation extends BaseComponent implements OnInit { if (error.status === 302) { this.uiNotificationService.snackBarNotification(this.language.instant('EMAIL-CONFIRMATION.EMAIL-FOUND'), SnackBarNotificationLevel.Warning); this.router.navigate(['home']); - } - else { - this.uiNotificationService.snackBarNotification(this.language.instant('EMAIL-CONFIRMATION.EXPIRED-EMAIL'), SnackBarNotificationLevel.Error); - this.router.navigate(['login']); + } else { + this.uiNotificationService.snackBarNotification(this.language.instant('EMAIL-CONFIRMATION.EXPIRED-EMAIL'), SnackBarNotificationLevel.Error); } } diff --git a/dmp-frontend/src/app/ui/user-profile/user-profile.component.ts b/dmp-frontend/src/app/ui/user-profile/user-profile.component.ts index 8b55e5be2..85b969eee 100644 --- a/dmp-frontend/src/app/ui/user-profile/user-profile.component.ts +++ b/dmp-frontend/src/app/ui/user-profile/user-profile.component.ts @@ -160,6 +160,7 @@ export class UserProfileComponent extends BaseComponent implements OnInit, OnDes nameof(x => x.createdAt), nameof(x => x.updatedAt), nameof(x => x.hash), + `${nameof(x => x.credentials)}.${nameof(x => x.id)}`, `${nameof(x => x.credentials)}.${nameof(x => x.data.email)}`, `${nameof(x => x.credentials)}.${nameof(x => x.data.externalProviderNames)}`, ] @@ -326,28 +327,15 @@ export class UserProfileComponent extends BaseComponent implements OnInit, OnDes .subscribe(confirm => { if (confirm) { - this.userService.removeCredentialAccount({ credentialId: userCredential.user.id }).subscribe(result => { - //TODO + this.userService.removeCredentialAccount({ credentialId: userCredential.id }).subscribe(result => { + this.dialog.open(PopupNotificationDialogComponent, { + data: { + title: this.language.instant('USER-PROFILE.UNLINK-ACCOUNT.TITLE'), + message: this.language.instant('USER-PROFILE.UNLINK-ACCOUNT.MESSAGE', {'accountToBeUnlinked': userCredential.data?.email}) + }, maxWidth: '30em' + }); }, error => this.onCallbackError(error)); - - //TODO: refactor - // const unlinkAccountModel: UnlinkAccountRequestModel = { - // userId: this.currentUserId, - // email: userCredential.email, - // provider: userCredential.provider - // }; - // this.unlinkAccountEmailConfirmation.sendConfirmationEmail(unlinkAccountModel).pipe(takeUntil(this._destroyed)).subscribe( - // result => { - // this.dialog.open(PopupNotificationDialogComponent, { - // data: { - // title: this.language.instant('USER-PROFILE.UNLINK-ACCOUNT.TITLE'), - // message: this.language.instant('USER-PROFILE.UNLINK-ACCOUNT.MESSAGE', { 'accountToBeUnlinked': userCredential.email }) - // }, maxWidth: '35em' - // }); - // }, - // error => { this.onCallbackError(error); } - // ); } }); } diff --git a/dmp-frontend/src/app/ui/user-profile/user-profile.module.ts b/dmp-frontend/src/app/ui/user-profile/user-profile.module.ts index 9a6cf89d2..a861551e8 100644 --- a/dmp-frontend/src/app/ui/user-profile/user-profile.module.ts +++ b/dmp-frontend/src/app/ui/user-profile/user-profile.module.ts @@ -4,9 +4,6 @@ import { AutoCompleteModule } from '@app/library/auto-complete/auto-complete.mod import { CommonFormsModule } from '@common/forms/common-forms.module'; import { FormValidationErrorsDialogModule } from '@common/forms/form-validation-errors-dialog/form-validation-errors-dialog.module'; import { CommonUiModule } from '@common/ui/common-ui.module'; -import { LoginComponent } from '../auth/login/login.component'; -import { LoginModule } from '../auth/login/login.module'; -import { AddAccountDialogComponent } from './add-account/add-account-dialog.component'; import { AddAccountDialogModule } from './add-account/add-account-dialog.module'; import { UserProfileComponent } from './user-profile.component'; import { UserProfileRoutingModule } from './user-profile.routing'; diff --git a/dmp-frontend/src/assets/i18n/en.json b/dmp-frontend/src/assets/i18n/en.json index 5c02a4697..da2bcdad2 100644 --- a/dmp-frontend/src/assets/i18n/en.json +++ b/dmp-frontend/src/assets/i18n/en.json @@ -2152,5 +2152,14 @@ "ACTIONS": { "CONFIRM": "Confirm" } + }, + "UNLINK-ACCOUNT": { + "TITLE": "Unlink Your Account", + "MESSAGES": { + "CONFIRMATION": "Are you sure that you want to unlink this account?" + }, + "ACTIONS": { + "CONFIRM": "Confirm" + } } } diff --git a/notification-service/notification-web/src/main/resources/notification_templates/mergeacountconfirmation/email/body.en.html b/notification-service/notification-web/src/main/resources/notification_templates/mergeacountconfirmation/email/body.en.html index a2c31107c..8e60badea 100644 --- a/notification-service/notification-web/src/main/resources/notification_templates/mergeacountconfirmation/email/body.en.html +++ b/notification-service/notification-web/src/main/resources/notification_templates/mergeacountconfirmation/email/body.en.html @@ -271,7 +271,7 @@ - +
Confirm Merge Request Confirm Merge Request
diff --git a/notification-service/notification-web/src/main/resources/notification_templates/mergeacountconfirmation/inapp/body.en.html b/notification-service/notification-web/src/main/resources/notification_templates/mergeacountconfirmation/inapp/body.en.html index ce2f63d0d..fd2e7a0c7 100644 --- a/notification-service/notification-web/src/main/resources/notification_templates/mergeacountconfirmation/inapp/body.en.html +++ b/notification-service/notification-web/src/main/resources/notification_templates/mergeacountconfirmation/inapp/body.en.html @@ -9,6 +9,6 @@

User {userName} have sent you a merge Request.

Please confirm that you want to merge your {installation-url} account with that account.
The link will expire in {expiration_time}.

- Confirm Merge Request + Confirm Merge Request \ No newline at end of file diff --git a/notification-service/notification-web/src/main/resources/notification_templates/removecredentialconfirmation/email/body.en.html b/notification-service/notification-web/src/main/resources/notification_templates/removecredentialconfirmation/email/body.en.html index cd3ff1e7e..3f0b3ac2d 100644 --- a/notification-service/notification-web/src/main/resources/notification_templates/removecredentialconfirmation/email/body.en.html +++ b/notification-service/notification-web/src/main/resources/notification_templates/removecredentialconfirmation/email/body.en.html @@ -271,7 +271,7 @@ - +
Confirm Unlink Request Confirm Unlink Request
diff --git a/notification-service/notification-web/src/main/resources/notification_templates/removecredentialconfirmation/inapp/body.en.html b/notification-service/notification-web/src/main/resources/notification_templates/removecredentialconfirmation/inapp/body.en.html index e9a3d62be..0db960f3c 100644 --- a/notification-service/notification-web/src/main/resources/notification_templates/removecredentialconfirmation/inapp/body.en.html +++ b/notification-service/notification-web/src/main/resources/notification_templates/removecredentialconfirmation/inapp/body.en.html @@ -9,6 +9,6 @@

You have made a request to unlink your email account in ARGOS.

Please confirm that you want to unlink your {email} account.
The link will expire in {expiration_time}.

- Confirm Unlink Request + Confirm Unlink Request \ No newline at end of file diff --git a/notification-service/notification/src/main/java/gr/cite/notification/integrationevent/inbox/notify/NotifyConsistencyHandler.java b/notification-service/notification/src/main/java/gr/cite/notification/integrationevent/inbox/notify/NotifyConsistencyHandler.java index 9af7e1cc8..4b59fd929 100644 --- a/notification-service/notification/src/main/java/gr/cite/notification/integrationevent/inbox/notify/NotifyConsistencyHandler.java +++ b/notification-service/notification/src/main/java/gr/cite/notification/integrationevent/inbox/notify/NotifyConsistencyHandler.java @@ -1,11 +1,15 @@ package gr.cite.notification.integrationevent.inbox.notify; import gr.cite.notification.common.StringUtils; +import gr.cite.notification.common.enums.ContactInfoType; import gr.cite.notification.common.enums.IsActive; import gr.cite.notification.common.enums.NotificationContactType; +import gr.cite.notification.data.UserContactInfoEntity; import gr.cite.notification.integrationevent.inbox.ConsistencyHandler; import gr.cite.notification.model.User; +import gr.cite.notification.model.UserContactInfo; import gr.cite.notification.model.builder.UserBuilder; +import gr.cite.notification.query.UserContactInfoQuery; import gr.cite.notification.query.UserQuery; import gr.cite.tools.data.builder.BuilderFactory; import gr.cite.tools.data.query.QueryFactory; @@ -39,9 +43,10 @@ public class NotifyConsistencyHandler implements ConsistencyHandler