From bee0e9f152b9c64e102627b8c6ab3027701877c5 Mon Sep 17 00:00:00 2001 From: amentis Date: Wed, 6 Dec 2023 13:08:49 +0200 Subject: [PATCH] fix update_notification script --- .../eu/eudat/authorization/Permission.java | 6 + .../tenant/MultitenancyConfiguration.java | 9 + .../scope/tenant/MultitenancyProperties.java | 16 ++ .../commons/scope/tenant/TenantScope.java | 91 ++++++++ .../commons/scope/tenant/TenantScoped.java | 8 + .../eu/eudat/data/TenantEntityManager.java | 93 ++++++++ .../java/eu/eudat/data/TenantUserEntity.java | 85 +++++++ .../java/eu/eudat/data/old/Notification.java | 2 +- .../eudat/data/tenant/TenantFilterAspect.java | 40 ++++ .../eu/eudat/data/tenant/TenantListener.java | 61 +++++ .../data/tenant/TenantScopedBaseEntity.java | 36 +++ .../main/java/eu/eudat/model/TenantUser.java | 86 ++++++++ .../model/builder/TenantUserBuilder.java | 138 ++++++++++++ .../model/censorship/TenantUserCensor.java | 51 +++++ .../model/deleter/TenantUserDeleter.java | 74 +++++++ .../java/eu/eudat/query/TenantUserQuery.java | 208 ++++++++++++++++++ .../eudat/query/lookup/TenantUserLookup.java | 68 ++++++ .../src/main/resources/config/permissions.yml | 29 ++- .../web/src/main/resources/config/tenant.yml | 4 +- 19 files changed, 1102 insertions(+), 3 deletions(-) create mode 100644 dmp-backend/core/src/main/java/eu/eudat/commons/scope/tenant/MultitenancyConfiguration.java create mode 100644 dmp-backend/core/src/main/java/eu/eudat/commons/scope/tenant/MultitenancyProperties.java create mode 100644 dmp-backend/core/src/main/java/eu/eudat/commons/scope/tenant/TenantScope.java create mode 100644 dmp-backend/core/src/main/java/eu/eudat/commons/scope/tenant/TenantScoped.java create mode 100644 dmp-backend/core/src/main/java/eu/eudat/data/TenantEntityManager.java create mode 100644 dmp-backend/core/src/main/java/eu/eudat/data/TenantUserEntity.java create mode 100644 dmp-backend/core/src/main/java/eu/eudat/data/tenant/TenantFilterAspect.java create mode 100644 dmp-backend/core/src/main/java/eu/eudat/data/tenant/TenantListener.java create mode 100644 dmp-backend/core/src/main/java/eu/eudat/data/tenant/TenantScopedBaseEntity.java create mode 100644 dmp-backend/core/src/main/java/eu/eudat/model/TenantUser.java create mode 100644 dmp-backend/core/src/main/java/eu/eudat/model/builder/TenantUserBuilder.java create mode 100644 dmp-backend/core/src/main/java/eu/eudat/model/censorship/TenantUserCensor.java create mode 100644 dmp-backend/core/src/main/java/eu/eudat/model/deleter/TenantUserDeleter.java create mode 100644 dmp-backend/core/src/main/java/eu/eudat/query/TenantUserQuery.java create mode 100644 dmp-backend/core/src/main/java/eu/eudat/query/lookup/TenantUserLookup.java diff --git a/dmp-backend/core/src/main/java/eu/eudat/authorization/Permission.java b/dmp-backend/core/src/main/java/eu/eudat/authorization/Permission.java index ac7fe2b4f..13232020c 100644 --- a/dmp-backend/core/src/main/java/eu/eudat/authorization/Permission.java +++ b/dmp-backend/core/src/main/java/eu/eudat/authorization/Permission.java @@ -154,6 +154,12 @@ public final class Permission { public static String BrowseTenant = "BrowseTenant"; public static String EditTenant= "EditTenant"; public static String DeleteTenant = "DeleteTenant"; + public static String AllowNoTenant = "AllowNoTenant"; + + //TenantUser + public static String BrowseTenantUser = "BrowseTenantUser"; + public static String EditTenantUser = "EditTenantUser"; + public static String DeleteTenantUser = "DeleteTenantUser"; } diff --git a/dmp-backend/core/src/main/java/eu/eudat/commons/scope/tenant/MultitenancyConfiguration.java b/dmp-backend/core/src/main/java/eu/eudat/commons/scope/tenant/MultitenancyConfiguration.java new file mode 100644 index 000000000..68a9f9306 --- /dev/null +++ b/dmp-backend/core/src/main/java/eu/eudat/commons/scope/tenant/MultitenancyConfiguration.java @@ -0,0 +1,9 @@ +package eu.eudat.commons.scope.tenant; + +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@EnableConfigurationProperties(MultitenancyProperties.class) +public class MultitenancyConfiguration { +} diff --git a/dmp-backend/core/src/main/java/eu/eudat/commons/scope/tenant/MultitenancyProperties.java b/dmp-backend/core/src/main/java/eu/eudat/commons/scope/tenant/MultitenancyProperties.java new file mode 100644 index 000000000..79b9ed745 --- /dev/null +++ b/dmp-backend/core/src/main/java/eu/eudat/commons/scope/tenant/MultitenancyProperties.java @@ -0,0 +1,16 @@ +package eu.eudat.commons.scope.tenant; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "tenant.multitenancy") +public class MultitenancyProperties { + private boolean isMultitenant; + + public boolean isMultitenant() { + return isMultitenant; + } + + public void setIsMultitenant(boolean multitenant) { + isMultitenant = multitenant; + } +} diff --git a/dmp-backend/core/src/main/java/eu/eudat/commons/scope/tenant/TenantScope.java b/dmp-backend/core/src/main/java/eu/eudat/commons/scope/tenant/TenantScope.java new file mode 100644 index 000000000..bc428a508 --- /dev/null +++ b/dmp-backend/core/src/main/java/eu/eudat/commons/scope/tenant/TenantScope.java @@ -0,0 +1,91 @@ +package eu.eudat.commons.scope.tenant; + +import eu.eudat.data.tenant.TenantScopedBaseEntity; +import gr.cite.tools.logging.LoggerService; +import jakarta.persistence.EntityManager; +import org.hibernate.Session; +import org.slf4j.LoggerFactory; +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; + +@Component +//@RequestScope +public class TenantScope { + public static final String TenantReplaceParameter = "::TenantCode::"; + public static final String TenantCodesClaimName = "TenantCodes"; + public static final String TenantClaimName = "x-tenant"; + + private final MultitenancyProperties multitenancy; + private static final LoggerService logger = new LoggerService(LoggerFactory.getLogger(TenantScope.class)); + private final AtomicReference tenant = new AtomicReference<>(); + private final AtomicReference tenantCode = new AtomicReference<>(); + private final AtomicReference initialTenant = new AtomicReference<>(); + private final AtomicReference initialTenantCode = new AtomicReference<>(); + + @Autowired + public TenantScope(MultitenancyProperties multitenancy) { + this.multitenancy = multitenancy; + } + + public Boolean isMultitenant() { + return multitenancy.isMultitenant(); + } + + public Boolean isSet() { + if (!this.isMultitenant()) + return Boolean.TRUE; + return this.tenant.get() != null; + } + + public UUID getTenant() throws InvalidApplicationException { + if (!this.isMultitenant()) + return null; + if (this.tenant.get() == null) + throw new InvalidApplicationException("tenant not set"); + return this.tenant.get(); + } + + public String getTenantCode() throws InvalidApplicationException { + if (!this.isMultitenant()) + return null; + if (this.tenant.get() == null) + throw new InvalidApplicationException("tenant not set"); + return this.tenantCode.get(); + } + + public void setTempTenant(EntityManager entityManager, UUID tenant) { + this.tenant.set(tenant); + + if (this.tenant.get() != null) { + entityManager + .unwrap(Session.class) + .enableFilter(TenantScopedBaseEntity.tenantFilter).setParameter(TenantScopedBaseEntity.tenantFilterTenantParam, this.tenant.get().toString()); + } + } + + public void removeTempTenant(EntityManager entityManager) { + this.tenant.set(this.initialTenant.get()); + this.tenantCode.set(this.initialTenantCode.get()); + if (this.initialTenant.get() != null) { + entityManager + .unwrap(Session.class) + .enableFilter(TenantScopedBaseEntity.tenantFilter).setParameter(TenantScopedBaseEntity.tenantFilterTenantParam, this.initialTenant.get().toString()); + } + } + + public void setTenant(UUID tenant, String tenantCode) { + if (this.isMultitenant()) { + this.tenant.set(tenant); + this.initialTenant.set(tenant); + this.tenantCode.set(tenantCode); + this.initialTenantCode.set(tenantCode); + } + } +} + diff --git a/dmp-backend/core/src/main/java/eu/eudat/commons/scope/tenant/TenantScoped.java b/dmp-backend/core/src/main/java/eu/eudat/commons/scope/tenant/TenantScoped.java new file mode 100644 index 000000000..41c85c469 --- /dev/null +++ b/dmp-backend/core/src/main/java/eu/eudat/commons/scope/tenant/TenantScoped.java @@ -0,0 +1,8 @@ +package eu.eudat.commons.scope.tenant; + +import java.util.UUID; + +public interface TenantScoped { + void setTenantId(UUID tenantId); + UUID getTenantId(); +} diff --git a/dmp-backend/core/src/main/java/eu/eudat/data/TenantEntityManager.java b/dmp-backend/core/src/main/java/eu/eudat/data/TenantEntityManager.java new file mode 100644 index 000000000..d06248cc6 --- /dev/null +++ b/dmp-backend/core/src/main/java/eu/eudat/data/TenantEntityManager.java @@ -0,0 +1,93 @@ +package eu.eudat.data; + +import eu.eudat.authorization.Permission; +import eu.eudat.commons.scope.tenant.TenantScope; +import eu.eudat.commons.scope.tenant.TenantScoped; +import gr.cite.commons.web.authz.service.AuthorizationService; +import gr.cite.commons.web.oidc.principal.CurrentPrincipalResolver; +import gr.cite.commons.web.oidc.principal.extractor.ClaimExtractor; +import gr.cite.tools.exception.MyForbiddenException; + +import jakarta.persistence.*; +import org.springframework.stereotype.Service; +import org.springframework.web.context.annotation.RequestScope; + +import javax.management.InvalidApplicationException; + +import java.util.UUID; + +@Service +@RequestScope +public class TenantEntityManager { + @PersistenceContext + private EntityManager entityManager; + private final CurrentPrincipalResolver currentPrincipalResolver; + private final ClaimExtractor claimExtractor; + private final AuthorizationService authorizationService; + private final TenantScope tenantScope; + + public TenantEntityManager(CurrentPrincipalResolver currentPrincipalResolver, ClaimExtractor claimExtractor, AuthorizationService authorizationService, TenantScope tenantScope) { + this.currentPrincipalResolver = currentPrincipalResolver; + this.claimExtractor = claimExtractor; + this.authorizationService = authorizationService; + this.tenantScope = tenantScope; + } + + + public void persist(Object entity) { + this.entityManager.persist(entity); + } + + public T merge(T entity) throws InvalidApplicationException { + if (tenantScope.isMultitenant() && (entity instanceof TenantScoped)) { + this.currentPrincipalResolver.currentPrincipal().isAuthenticated(); + this.claimExtractor.subjectUUID(this.currentPrincipalResolver.currentPrincipal()); + boolean isAllowedNoTenant = authorizationService.authorize(Permission.AllowNoTenant); + + final UUID tenantId = !isAllowedNoTenant ? tenantScope.getTenant() : null; + if (!isAllowedNoTenant && !tenantId.equals(((TenantScoped) entity).getTenantId())) throw new MyForbiddenException("tenant tampering"); + } + return this.entityManager.merge(entity); + } + + public void remove(Object entity) throws InvalidApplicationException { + if (tenantScope.isMultitenant() && (entity instanceof TenantScoped)) { + final UUID tenantId = tenantScope.getTenant(); + if (!tenantId.equals(((TenantScoped) entity).getTenantId())) throw new MyForbiddenException("tenant tampering"); + } + this.entityManager.remove(entity); + } + + public T find(Class entityClass, Object primaryKey) throws InvalidApplicationException { + T entity = this.entityManager.find(entityClass, primaryKey); + + if (tenantScope.isMultitenant() && (entity instanceof TenantScoped)) { + this.currentPrincipalResolver.currentPrincipal().isAuthenticated(); + this.claimExtractor.subjectUUID(this.currentPrincipalResolver.currentPrincipal()); + boolean isAllowedNoTenant = authorizationService.authorize(Permission.AllowNoTenant); + + final UUID tenantId = !isAllowedNoTenant ? tenantScope.getTenant() : null; + if (!isAllowedNoTenant && !tenantId.equals(((TenantScoped) entity).getTenantId())) return null; + } + return entity; + } + + public void flush() { + this.entityManager.flush(); + } + + + public void setFlushMode(FlushModeType flushMode) { + this.entityManager.setFlushMode(flushMode); + + } + + public FlushModeType getFlushMode() { + return this.entityManager.getFlushMode(); + } + + public void clear() { + this.entityManager.clear(); + } + +} diff --git a/dmp-backend/core/src/main/java/eu/eudat/data/TenantUserEntity.java b/dmp-backend/core/src/main/java/eu/eudat/data/TenantUserEntity.java new file mode 100644 index 000000000..a624f0f70 --- /dev/null +++ b/dmp-backend/core/src/main/java/eu/eudat/data/TenantUserEntity.java @@ -0,0 +1,85 @@ +package eu.eudat.data; + +import eu.eudat.commons.enums.IsActive; +import eu.eudat.data.converters.enums.IsActiveConverter; +import jakarta.persistence.*; +import java.time.Instant; +import java.util.UUID; + +@Entity +@Table(name = "\"TenantUser\"") +public class TenantUserEntity { + @Id + @Column(name = "id", columnDefinition = "uuid", updatable = false, nullable = false) + private UUID id; + public final static String _id = "id"; + + @Column(name = "user", columnDefinition = "uuid", updatable = false, nullable = false) + private UUID userId; + public final static String _userId = "userId"; + + @Column(name = "tenant", columnDefinition = "uuid", updatable = false, nullable = false) + private UUID tenantId; + public final static String _tenantId = "tenantId"; + + @Column(name = "is_active", length = 20, nullable = false) + @Convert(converter = IsActiveConverter.class) + private IsActive isActive; + public final static String _isActive = "isActive"; + + @Column(name = "created_at", nullable = false) + private Instant createdAt; + public final static String _createdAt = "createdAt"; + + @Column(name = "updated_at", nullable = false) + private Instant updatedAt; + public final static String _updatedAt = "updatedAt"; + + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } + + public UUID getUserId() { + return userId; + } + + public void setUserId(UUID userId) { + this.userId = userId; + } + + public UUID getTenantId() { + return tenantId; + } + + public void setTenantId(UUID tenantId) { + this.tenantId = tenantId; + } + + public IsActive getIsActive() { + return isActive; + } + + public void setIsActive(IsActive isActive) { + this.isActive = isActive; + } + + public Instant getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(Instant createdAt) { + this.createdAt = createdAt; + } + + public Instant getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(Instant updatedAt) { + this.updatedAt = updatedAt; + } +} diff --git a/dmp-backend/core/src/main/java/eu/eudat/data/old/Notification.java b/dmp-backend/core/src/main/java/eu/eudat/data/old/Notification.java index 0a3ed6ee5..35d6f76f4 100644 --- a/dmp-backend/core/src/main/java/eu/eudat/data/old/Notification.java +++ b/dmp-backend/core/src/main/java/eu/eudat/data/old/Notification.java @@ -13,7 +13,7 @@ import java.util.List; import java.util.UUID; @Entity -@Table(name = "\"Notification\"") +@Table(name = "\"NotificationLegacy\"") public class Notification implements DataEntity { @Id diff --git a/dmp-backend/core/src/main/java/eu/eudat/data/tenant/TenantFilterAspect.java b/dmp-backend/core/src/main/java/eu/eudat/data/tenant/TenantFilterAspect.java new file mode 100644 index 000000000..f63a4f567 --- /dev/null +++ b/dmp-backend/core/src/main/java/eu/eudat/data/tenant/TenantFilterAspect.java @@ -0,0 +1,40 @@ +package eu.eudat.data.tenant; + +import eu.eudat.commons.scope.tenant.TenantScope; +import jakarta.persistence.EntityManager; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.AfterReturning; +import org.aspectj.lang.annotation.Aspect; +import org.hibernate.Session; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.management.InvalidApplicationException; + + +//@Aspect +@Component +public class TenantFilterAspect { + + private final TenantScope tenantScope; + + @Autowired + public TenantFilterAspect( + TenantScope tenantScope + ) { + this.tenantScope = tenantScope; + } + + @AfterReturning( + pointcut = "bean(entityManagerFactory) && execution(* createEntityManager(..))", + returning = "retVal") + public void getSessionAfter(JoinPoint joinPoint, Object retVal) throws InvalidApplicationException { + if (retVal != null && retVal instanceof EntityManager && tenantScope.isSet()) { + Session session = ((EntityManager) retVal).unwrap(Session.class); + session + .enableFilter(TenantScopedBaseEntity.tenantFilter) + .setParameter(TenantScopedBaseEntity.tenantFilterTenantParam, tenantScope.getTenant().toString()); + } + } + +} diff --git a/dmp-backend/core/src/main/java/eu/eudat/data/tenant/TenantListener.java b/dmp-backend/core/src/main/java/eu/eudat/data/tenant/TenantListener.java new file mode 100644 index 000000000..f11a2b93d --- /dev/null +++ b/dmp-backend/core/src/main/java/eu/eudat/data/tenant/TenantListener.java @@ -0,0 +1,61 @@ +package eu.eudat.data.tenant; + + +import eu.eudat.commons.scope.tenant.TenantScope; +import eu.eudat.commons.scope.tenant.TenantScoped; +import gr.cite.tools.exception.MyForbiddenException; +import gr.cite.tools.logging.LoggerService; +import jakarta.persistence.PrePersist; +import jakarta.persistence.PreRemove; +import jakarta.persistence.PreUpdate; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; + +import javax.management.InvalidApplicationException; + +import java.util.UUID; + +public class TenantListener { + private static final LoggerService logger = new LoggerService(LoggerFactory.getLogger(TenantListener.class)); + private final TenantScope tenantScope; + + @Autowired + public TenantListener( + TenantScope tenantScope + ) { + this.tenantScope = tenantScope; + } + + @PrePersist + public void setTenantOnCreate(TenantScoped entity) throws InvalidApplicationException { + if (tenantScope.isMultitenant()) { + final UUID tenantId = tenantScope.getTenant(); + entity.setTenantId(tenantId); + } else { + entity.setTenantId(null); + } + } + + @PreUpdate + @PreRemove + public void setTenantOnUpdate(TenantScoped entity) throws InvalidApplicationException { + if (tenantScope.isMultitenant()) { + if (entity.getTenantId() == null) { + logger.error("somebody tried to set null tenant"); + throw new MyForbiddenException("tenant tampering"); + } + if (entity.getTenantId().compareTo(tenantScope.getTenant()) != 0) { + logger.error("somebody tried to change an entries tenant"); + throw new MyForbiddenException("tenant tampering"); + } + + final UUID tenantId = tenantScope.getTenant(); + entity.setTenantId(tenantId); + } else { + if (entity.getTenantId() != null) { + logger.error("somebody tried to set non null tenant"); + throw new MyForbiddenException("tenant tampering"); + } + } + } +} diff --git a/dmp-backend/core/src/main/java/eu/eudat/data/tenant/TenantScopedBaseEntity.java b/dmp-backend/core/src/main/java/eu/eudat/data/tenant/TenantScopedBaseEntity.java new file mode 100644 index 000000000..e4aca7957 --- /dev/null +++ b/dmp-backend/core/src/main/java/eu/eudat/data/tenant/TenantScopedBaseEntity.java @@ -0,0 +1,36 @@ +package eu.eudat.data.tenant; + +import eu.eudat.commons.scope.tenant.TenantScoped; +import jakarta.persistence.*; +import org.hibernate.annotations.Filter; +import org.hibernate.annotations.FilterDef; +import org.hibernate.annotations.ParamDef; + + +import java.io.Serializable; +import java.util.UUID; + +@MappedSuperclass +//@Getter +//@Setter +//@NoArgsConstructor +@FilterDef(name = TenantScopedBaseEntity.tenantFilter, parameters = {@ParamDef(name = TenantScopedBaseEntity.tenantFilterTenantParam, type = String.class)}) +@Filter(name = "tenantFilter", condition = "tenant = (cast(:tenantId as uuid))") +@EntityListeners(TenantListener.class) +public abstract class TenantScopedBaseEntity implements TenantScoped, Serializable { + private static final long serialVersionUID = 1L; + public static final String tenantFilter = "tenantFilter"; + public static final String tenantFilterTenantParam = "tenantId"; + + @Column(name = "tenant", columnDefinition = "uuid", nullable = false) + private UUID tenantId; + public static final String _tenantId = "tenantId"; + public UUID getTenantId() { + return tenantId; + } + + @Override + public void setTenantId(UUID tenantId) { + this.tenantId = tenantId; + } +} diff --git a/dmp-backend/core/src/main/java/eu/eudat/model/TenantUser.java b/dmp-backend/core/src/main/java/eu/eudat/model/TenantUser.java new file mode 100644 index 000000000..aec6b0def --- /dev/null +++ b/dmp-backend/core/src/main/java/eu/eudat/model/TenantUser.java @@ -0,0 +1,86 @@ +package eu.eudat.model; + +import eu.eudat.commons.enums.IsActive; + +import java.time.Instant; +import java.util.UUID; + +public class TenantUser { + + private UUID id; + public final static String _id = "id"; + + private User user; + public final static String _user = "user"; + + private Tenant tenant; + public final static String _tenant = "tenant"; + + private IsActive isActive; + public final static String _isActive = "isActive"; + + private Instant createdAt; + public final static String _createdAt = "createdAt"; + + private Instant updatedAt; + public final static String _updatedAt = "updatedAt"; + + private String hash; + public final static String _hash = "hash"; + + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } + + public User getUser() { + return user; + } + + public void setUser(User user) { + this.user = user; + } + + public Tenant getTenant() { + return tenant; + } + + public void setTenant(Tenant tenant) { + this.tenant = tenant; + } + + public IsActive getIsActive() { + return isActive; + } + + public void setIsActive(IsActive isActive) { + this.isActive = isActive; + } + + public Instant getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(Instant createdAt) { + this.createdAt = createdAt; + } + + public Instant getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(Instant updatedAt) { + this.updatedAt = updatedAt; + } + + public String getHash() { + return hash; + } + + public void setHash(String hash) { + this.hash = hash; + } +} diff --git a/dmp-backend/core/src/main/java/eu/eudat/model/builder/TenantUserBuilder.java b/dmp-backend/core/src/main/java/eu/eudat/model/builder/TenantUserBuilder.java new file mode 100644 index 000000000..bfab8e748 --- /dev/null +++ b/dmp-backend/core/src/main/java/eu/eudat/model/builder/TenantUserBuilder.java @@ -0,0 +1,138 @@ +package eu.eudat.model.builder; + +import eu.eudat.authorization.AuthorizationFlags; +import eu.eudat.convention.ConventionService; +import eu.eudat.data.TenantUserEntity; +import eu.eudat.model.Tenant; +import eu.eudat.model.TenantUser; +import eu.eudat.model.User; +import eu.eudat.query.TenantQuery; +import eu.eudat.query.UserQuery; +import gr.cite.tools.data.builder.BuilderFactory; +import gr.cite.tools.data.query.QueryFactory; +import gr.cite.tools.exception.MyApplicationException; +import gr.cite.tools.fieldset.BaseFieldSet; +import gr.cite.tools.fieldset.FieldSet; +import gr.cite.tools.logging.DataLogEntry; +import gr.cite.tools.logging.LoggerService; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; + +import java.util.*; +import java.util.stream.Collectors; + +@Component +@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE) +public class TenantUserBuilder extends BaseBuilder { + + private final BuilderFactory builderFactory; + private final QueryFactory queryFactory; + + private EnumSet authorize = EnumSet.of(AuthorizationFlags.None); + + @Autowired + public TenantUserBuilder( + ConventionService conventionService, + BuilderFactory builderFactory, + QueryFactory queryFactory + ) { + super(conventionService, new LoggerService(LoggerFactory.getLogger(TenantUserBuilder.class))); + this.builderFactory = builderFactory; + this.queryFactory = queryFactory; + } + + public TenantUserBuilder authorize(EnumSet values) { + this.authorize = values; + return this; + } + + @Override + public List build(FieldSet fields, List datas) throws MyApplicationException { + this.logger.debug("building for {} items requesting {} fields", Optional.ofNullable(datas).map(e -> e.size()).orElse(0), Optional.ofNullable(fields).map(e -> e.getFields()).map(e -> e.size()).orElse(0)); + this.logger.trace(new DataLogEntry("requested fields", fields)); + if (fields == null || fields.isEmpty()) return new ArrayList<>(); + + FieldSet userFields = fields.extractPrefixed(this.asPrefix(TenantUser._user)); + Map userMap = this.collectUsers(userFields, datas); + + FieldSet tenantFields = fields.extractPrefixed(this.asPrefix(TenantUser._tenant)); + Map tenantMap = this.collectTenants(tenantFields, datas); + + List models = new ArrayList<>(); + + for (TenantUserEntity d : datas) { + TenantUser m = new TenantUser(); + if (fields.hasField(this.asIndexer(TenantUser._id))) m.setId(d.getId()); + if (fields.hasField(this.asIndexer(TenantUser._hash))) m.setHash(this.hashValue(d.getUpdatedAt())); + if (fields.hasField(this.asIndexer(TenantUser._createdAt))) m.setCreatedAt(d.getCreatedAt()); + if (fields.hasField(this.asIndexer(TenantUser._updatedAt))) m.setUpdatedAt(d.getUpdatedAt()); + if (fields.hasField(this.asIndexer(TenantUser._isActive))) m.setIsActive(d.getIsActive()); + if (!userFields.isEmpty() && userMap != null && userMap.containsKey(d.getUserId())) m.setUser(userMap.get(d.getUserId())); + if (!tenantFields.isEmpty() && tenantMap != null && tenantMap.containsKey(d.getTenantId())) m.setTenant(tenantMap.get(d.getTenantId())); + models.add(m); + } + this.logger.debug("build {} items", Optional.ofNullable(models).map(e -> e.size()).orElse(0)); + return models; + } + + private Map collectUsers(FieldSet fields, List datas) throws MyApplicationException { + if (fields.isEmpty() || datas.isEmpty()) return null; + this.logger.debug("checking related - {}", User.class.getSimpleName()); + + Map itemMap = null; + if (!fields.hasOtherField(this.asIndexer(User._id))) { + itemMap = this.asEmpty( + datas.stream().map(x -> x.getUserId()).distinct().collect(Collectors.toList()), + x -> { + User item = new User(); + item.setId(x); + return item; + }, + x -> x.getId()); + } else { + FieldSet clone = new BaseFieldSet(fields.getFields()).ensure(User._id); + UserQuery q = this.queryFactory.query(UserQuery.class).authorize(this.authorize).ids(datas.stream().map(x -> x.getUserId()).distinct().collect(Collectors.toList())); + itemMap = this.builderFactory.builder(UserBuilder.class).authorize(this.authorize).asForeignKey(q, clone, x -> x.getId()); + } + if (!fields.hasField(User._id)) { + itemMap.values().stream().filter(x -> x != null).map(x -> { + x.setId(null); + return x; + }).collect(Collectors.toList()); + } + + return itemMap; + } + + private Map collectTenants(FieldSet fields, List datas) throws MyApplicationException { + if (fields.isEmpty() || datas.isEmpty()) return null; + this.logger.debug("checking related - {}", Tenant.class.getSimpleName()); + + Map itemMap = null; + if (!fields.hasOtherField(this.asIndexer(Tenant._id))) { + itemMap = this.asEmpty( + datas.stream().map(x -> x.getTenantId()).distinct().collect(Collectors.toList()), + x -> { + Tenant item = new Tenant(); + item.setId(x); + return item; + }, + x -> x.getId()); + } else { + FieldSet clone = new BaseFieldSet(fields.getFields()).ensure(Tenant._id); + TenantQuery q = this.queryFactory.query(TenantQuery.class).authorize(this.authorize).ids(datas.stream().map(x -> x.getTenantId()).distinct().collect(Collectors.toList())); + itemMap = this.builderFactory.builder(TenantBuilder.class).authorize(this.authorize).asForeignKey(q, clone, x -> x.getId()); + } + if (!fields.hasField(Tenant._id)) { + itemMap.values().stream().filter(x -> x != null).map(x -> { + x.setId(null); + return x; + }).collect(Collectors.toList()); + } + + return itemMap; + } +} diff --git a/dmp-backend/core/src/main/java/eu/eudat/model/censorship/TenantUserCensor.java b/dmp-backend/core/src/main/java/eu/eudat/model/censorship/TenantUserCensor.java new file mode 100644 index 000000000..5dd5326a2 --- /dev/null +++ b/dmp-backend/core/src/main/java/eu/eudat/model/censorship/TenantUserCensor.java @@ -0,0 +1,51 @@ +package eu.eudat.model.censorship; + +import eu.eudat.authorization.OwnedResource; +import eu.eudat.authorization.Permission; +import eu.eudat.convention.ConventionService; +import eu.eudat.model.TenantUser; +import gr.cite.commons.web.authz.service.AuthorizationService; +import gr.cite.tools.data.censor.CensorFactory; +import gr.cite.tools.exception.MyForbiddenException; +import gr.cite.tools.fieldset.FieldSet; +import gr.cite.tools.logging.DataLogEntry; +import gr.cite.tools.logging.LoggerService; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.UUID; + + +@Component +@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE) +public class TenantUserCensor extends BaseCensor { + private static final LoggerService logger = new LoggerService(LoggerFactory.getLogger(TenantUserCensor.class)); + + protected final AuthorizationService authService; + protected final CensorFactory censorFactory; + + @Autowired + public TenantUserCensor( + ConventionService conventionService, + AuthorizationService authService, + CensorFactory censorFactory + ) { + super(conventionService); + this.authService = authService; + this.censorFactory = censorFactory; + } + + public void censor(FieldSet fields, UUID userId) throws MyForbiddenException { + logger.debug(new DataLogEntry("censoring fields", fields)); + if (this.isEmpty(fields)) return; + this.authService.authorizeAtLeastOneForce(userId != null ? List.of(new OwnedResource(userId)) : null, Permission.BrowseTenantUser); + FieldSet tenantFields = fields.extractPrefixed(this.asIndexerPrefix(TenantUser._tenant)); + this.censorFactory.censor(TenantCensor.class).censor(tenantFields, null); + FieldSet userFields = fields.extractPrefixed(this.asIndexerPrefix(TenantUser._user)); + this.censorFactory.censor(UserCensor.class).censor(userFields, userId); + } +} diff --git a/dmp-backend/core/src/main/java/eu/eudat/model/deleter/TenantUserDeleter.java b/dmp-backend/core/src/main/java/eu/eudat/model/deleter/TenantUserDeleter.java new file mode 100644 index 000000000..d2c9d9036 --- /dev/null +++ b/dmp-backend/core/src/main/java/eu/eudat/model/deleter/TenantUserDeleter.java @@ -0,0 +1,74 @@ +package eu.eudat.model.deleter; + +import eu.eudat.commons.enums.IsActive; +import eu.eudat.data.TenantEntityManager; +import eu.eudat.data.TenantUserEntity; +import eu.eudat.query.TenantUserQuery; +import gr.cite.tools.data.deleter.Deleter; +import gr.cite.tools.data.deleter.DeleterFactory; +import gr.cite.tools.data.query.QueryFactory; +import gr.cite.tools.logging.LoggerService; +import gr.cite.tools.logging.MapLogEntry; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; + +import javax.management.InvalidApplicationException; +import java.time.Instant; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +@Component +@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE) +public class TenantUserDeleter implements Deleter { + private static final LoggerService logger = new LoggerService(LoggerFactory.getLogger(TenantUserDeleter.class)); + + private final TenantEntityManager entityManager; + private final QueryFactory queryFactory; + private final DeleterFactory deleterFactory; + + @Autowired + public TenantUserDeleter( + TenantEntityManager entityManager, + QueryFactory queryFactory, + DeleterFactory deleterFactory + ) { + this.entityManager = entityManager; + this.queryFactory = queryFactory; + this.deleterFactory = deleterFactory; + } + + public void deleteAndSaveByIds(List ids) throws InvalidApplicationException { + logger.debug(new MapLogEntry("collecting to delete").And("count", Optional.ofNullable(ids).map(e -> e.size()).orElse(0)).And("ids", ids)); + List datas = this.queryFactory.query(TenantUserQuery.class).ids(ids).collect(); + logger.trace("retrieved {} items", Optional.ofNullable(datas).map(e -> e.size()).orElse(0)); + this.deleteAndSave(datas); + } + + public void deleteAndSave(List datas) throws InvalidApplicationException { + logger.debug("will delete {} items", Optional.ofNullable(datas).map(e -> e.size()).orElse(0)); + this.delete(datas); + logger.trace("saving changes"); + this.entityManager.flush(); + logger.trace("changes saved"); + } + + public void delete(List datas) throws InvalidApplicationException { + logger.debug("will delete {} items", Optional.ofNullable(datas).map(x -> x.size()).orElse(0)); + if (datas == null || datas.isEmpty()) return; + + Instant now = Instant.now(); + + for (TenantUserEntity item : datas) { + logger.trace("deleting item {}", item.getId()); + item.setIsActive(IsActive.Inactive); + item.setUpdatedAt(now); + logger.trace("updating item"); + this.entityManager.merge(item); + logger.trace("updated item"); + } + } +} diff --git a/dmp-backend/core/src/main/java/eu/eudat/query/TenantUserQuery.java b/dmp-backend/core/src/main/java/eu/eudat/query/TenantUserQuery.java new file mode 100644 index 000000000..44a52e600 --- /dev/null +++ b/dmp-backend/core/src/main/java/eu/eudat/query/TenantUserQuery.java @@ -0,0 +1,208 @@ +package eu.eudat.query; + +import eu.eudat.authorization.AuthorizationFlags; +import eu.eudat.authorization.Permission; +import eu.eudat.commons.enums.IsActive; +import eu.eudat.commons.scope.user.UserScope; +import eu.eudat.data.TenantUserEntity; +import eu.eudat.data.UserEntity; +import eu.eudat.model.Tenant; +import eu.eudat.model.TenantUser; +import gr.cite.commons.web.authz.service.AuthorizationService; +import gr.cite.tools.data.query.FieldResolver; +import gr.cite.tools.data.query.QueryBase; +import gr.cite.tools.data.query.QueryContext; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.Predicate; +import jakarta.persistence.criteria.Subquery; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; + +import jakarta.persistence.*; +import java.time.Instant; +import java.util.*; + +@Component +@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE) +public class TenantUserQuery extends QueryBase { + + private Collection ids; + private Collection userIds; + private Collection tenantIds; + private Collection isActives; + private UserQuery userQuery; + private EnumSet authorize = EnumSet.of(AuthorizationFlags.None); + private final UserScope userScope; + private final AuthorizationService authService; + + public TenantUserQuery( + UserScope userScope, + AuthorizationService authService + ) { + this.userScope = userScope; + this.authService = authService; + } + + public TenantUserQuery ids(UUID value) { + this.ids = List.of(value); + return this; + } + + public TenantUserQuery ids(UUID... value) { + this.ids = Arrays.asList(value); + return this; + } + + public TenantUserQuery ids(Collection values) { + this.ids = values; + return this; + } + + public TenantUserQuery userIds(UUID value) { + this.userIds = List.of(value); + return this; + } + + public TenantUserQuery userIds(UUID... value) { + this.userIds = Arrays.asList(value); + return this; + } + + public TenantUserQuery userIds(Collection values) { + this.userIds = values; + return this; + } + + public TenantUserQuery tenantIds(UUID value) { + this.tenantIds = List.of(value); + return this; + } + + public TenantUserQuery tenantIds(UUID... value) { + this.tenantIds = Arrays.asList(value); + return this; + } + + public TenantUserQuery tenantIds(Collection values) { + this.tenantIds = values; + return this; + } + + public TenantUserQuery isActive(IsActive value) { + this.isActives = List.of(value); + return this; + } + + public TenantUserQuery isActive(IsActive... value) { + this.isActives = Arrays.asList(value); + return this; + } + + public TenantUserQuery isActive(Collection values) { + this.isActives = values; + return this; + } + + public TenantUserQuery userSubQuery(UserQuery subQuery) { + this.userQuery = subQuery; + return this; + } + + public TenantUserQuery authorize(EnumSet values) { + this.authorize = values; + return this; + } + + @Override + protected Class entityClass() { + return TenantUserEntity.class; + } + + @Override + protected Boolean isFalseQuery() { + return this.isEmpty(this.ids) || this.isEmpty(this.userIds) || this.isEmpty(this.tenantIds) || this.isEmpty(this.isActives) || this.isFalseQuery(this.userQuery); + } + + + @Override + protected Predicate applyAuthZ(QueryContext queryContext) { + if (this.authorize.contains(AuthorizationFlags.None)) return null; + if (this.authorize.contains(AuthorizationFlags.Permission) && this.authService.authorize(Permission.BrowseTenantUser)) return null; + UUID ownerId = null; + if (this.authorize.contains(AuthorizationFlags.Owner)) ownerId = this.userScope.getUserIdSafe(); + + List predicates = new ArrayList<>(); + if (ownerId != null) { + predicates.add(queryContext.CriteriaBuilder.equal(queryContext.Root.get(TenantUserEntity._userId), ownerId)); + } + if (predicates.size() > 0) { + Predicate[] predicatesArray = predicates.toArray(new Predicate[0]); + return queryContext.CriteriaBuilder.and(predicatesArray); + } else { + return queryContext.CriteriaBuilder.or(); //Creates a false query + } + } + + @Override + protected Predicate applyFilters(QueryContext queryContext) { + List predicates = new ArrayList<>(); + if (this.ids != null) { + CriteriaBuilder.In inClause = queryContext.CriteriaBuilder.in(queryContext.Root.get(TenantUserEntity._id)); + for (UUID item : this.ids) inClause.value(item); + predicates.add(inClause); + } + if (this.userIds != null) { + CriteriaBuilder.In inClause = queryContext.CriteriaBuilder.in(queryContext.Root.get(TenantUserEntity._userId)); + for (UUID item : this.userIds) inClause.value(item); + predicates.add(inClause); + } + if (this.tenantIds != null) { + CriteriaBuilder.In inClause = queryContext.CriteriaBuilder.in(queryContext.Root.get(TenantUserEntity._tenantId)); + for (UUID item : this.tenantIds) inClause.value(item); + predicates.add(inClause); + } + if (this.isActives != null) { + CriteriaBuilder.In inClause = queryContext.CriteriaBuilder.in(queryContext.Root.get(TenantUserEntity._isActive)); + for (IsActive item : this.isActives) inClause.value(item); + predicates.add(inClause); + } + if (this.userQuery != null) { + Subquery subQuery = queryContext.Query.subquery(this.userQuery.entityClass()); + this.applySubQuery(this.userQuery, queryContext.CriteriaBuilder, subQuery); + predicates.add(queryContext.CriteriaBuilder.in(queryContext.Root.get(TenantUserEntity._userId)).value(subQuery)); + } + if (predicates.size() > 0) { + Predicate[] predicatesArray = predicates.toArray(new Predicate[0]); + return queryContext.CriteriaBuilder.and(predicatesArray); + } else { + return null; + } + } + + @Override + protected TenantUserEntity convert(Tuple tuple, Set columns) { + TenantUserEntity item = new TenantUserEntity(); + item.setId(QueryBase.convertSafe(tuple, columns, TenantUserEntity._id, UUID.class)); + item.setUserId(QueryBase.convertSafe(tuple, columns, TenantUserEntity._userId, UUID.class)); + item.setTenantId(QueryBase.convertSafe(tuple, columns, TenantUserEntity._tenantId, UUID.class)); + item.setCreatedAt(QueryBase.convertSafe(tuple, columns, TenantUserEntity._createdAt, Instant.class)); + item.setUpdatedAt(QueryBase.convertSafe(tuple, columns, TenantUserEntity._updatedAt, Instant.class)); + item.setIsActive(QueryBase.convertSafe(tuple, columns, TenantUserEntity._isActive, IsActive.class)); + return item; + } + + @Override + protected String fieldNameOf(FieldResolver item) { + if (item.match(TenantUser._id)) return TenantUserEntity._id; + else if (item.match(TenantUser._tenant, Tenant._id)) return TenantUserEntity._tenantId; + else if (item.prefix(TenantUser._tenant)) return TenantUserEntity._tenantId; + else if (item.match(TenantUser._isActive)) return TenantUserEntity._isActive; + else if (item.match(TenantUser._createdAt)) return TenantUserEntity._createdAt; + else if (item.match(TenantUser._updatedAt)) return TenantUserEntity._updatedAt; + else if (item.match(TenantUser._hash)) return TenantUserEntity._updatedAt; + else if (item.match(TenantUser._user, UserEntity._id)) return TenantUserEntity._userId; + else if (item.prefix(TenantUser._user)) return TenantUserEntity._userId; + else return null; + } +} diff --git a/dmp-backend/core/src/main/java/eu/eudat/query/lookup/TenantUserLookup.java b/dmp-backend/core/src/main/java/eu/eudat/query/lookup/TenantUserLookup.java new file mode 100644 index 000000000..10a7064f8 --- /dev/null +++ b/dmp-backend/core/src/main/java/eu/eudat/query/lookup/TenantUserLookup.java @@ -0,0 +1,68 @@ +package eu.eudat.query.lookup; + + +import eu.eudat.commons.enums.IsActive; +import eu.eudat.query.TenantUserQuery; +import gr.cite.tools.data.query.Lookup; +import gr.cite.tools.data.query.QueryFactory; + +import java.util.List; +import java.util.UUID; + +public class TenantUserLookup extends Lookup { + private List isActive; + private List ids; + private List userIds; + private List tenantIds; + + private UserLookup userSubQuery; + + public List getIsActive() { + return isActive; + } + + public void setIsActive(List isActive) { + this.isActive = isActive; + } + + public List getIds() { + return ids; + } + + public void setIds(List ids) { + this.ids = ids; + } + + public List getUserIds() { return userIds; } + + public void setUserIds(List userIds) { this.userIds = userIds; } + + public List getTenantIds() { + return tenantIds; + } + + public void setTenantIds(List tenantIds) { + this.tenantIds = tenantIds; + } + + public UserLookup getUserSubQuery() { + return userSubQuery; + } + + public void setUserSubQuery(UserLookup userSubQuery) { + this.userSubQuery = userSubQuery; + } + + public TenantUserQuery enrich(QueryFactory queryFactory) { + TenantUserQuery query = queryFactory.query(TenantUserQuery.class); + if (this.isActive != null) query.isActive(this.isActive); + if (this.ids != null) query.ids(this.ids); + if (this.userIds != null) query.userIds(this.userIds); + if (this.tenantIds != null) query.tenantIds(this.tenantIds); + if (this.userSubQuery != null) query.userSubQuery(this.userSubQuery.enrich(queryFactory)); + + this.enrichCommon(query); + + return query; + } +} diff --git a/dmp-backend/web/src/main/resources/config/permissions.yml b/dmp-backend/web/src/main/resources/config/permissions.yml index 9e9c80a7f..75a32ef8f 100644 --- a/dmp-backend/web/src/main/resources/config/permissions.yml +++ b/dmp-backend/web/src/main/resources/config/permissions.yml @@ -523,7 +523,7 @@ permissions: allowAnonymous: false allowAuthenticated: false - # ReferenceType Permissions + # Tenant Permissions BrowseTenant: roles: - Admin @@ -543,7 +543,34 @@ permissions: clients: [ ] allowAnonymous: false allowAuthenticated: false + AllowNoTenant: + roles: + - Admin + claims: [ ] + clients: [ ] + allowAnonymous: false + allowAuthenticated: false + # TenantUser Permissions + BrowseTenantUser: + roles: + - Admin + clients: [ ] + allowAnonymous: false + allowAuthenticated: false + EditTenantUser: + roles: + - Admin + clients: [ ] + allowAnonymous: false + allowAuthenticated: false + DeleteTenantUser: + roles: + - Admin + claims: [ ] + clients: [ ] + allowAnonymous: false + allowAuthenticated: false # DmpDescriptionTemplate Permissions BrowseDmpDescriptionTemplate: diff --git a/dmp-backend/web/src/main/resources/config/tenant.yml b/dmp-backend/web/src/main/resources/config/tenant.yml index ce3c2cdcd..8e2dbd774 100644 --- a/dmp-backend/web/src/main/resources/config/tenant.yml +++ b/dmp-backend/web/src/main/resources/config/tenant.yml @@ -1,3 +1,5 @@ tenant: configEncryptionAesKey: rmpTvZnRWzyisUtFADBcZCn0q7Z75Xdz - configEncryptionAesIv: ec05d521a23f80ad \ No newline at end of file + configEncryptionAesIv: ec05d521a23f80ad + multitenancy: + is-multitenant: false \ No newline at end of file