diff --git a/dmp-backend/core/src/main/java/eu/eudat/audit/AuditableAction.java b/dmp-backend/core/src/main/java/eu/eudat/audit/AuditableAction.java index eb57acfcc..684724804 100644 --- a/dmp-backend/core/src/main/java/eu/eudat/audit/AuditableAction.java +++ b/dmp-backend/core/src/main/java/eu/eudat/audit/AuditableAction.java @@ -96,6 +96,12 @@ public class AuditableAction { public static final EventId Dashboard_MyRecentActivityItems = new EventId(15000, "Dashboard_MyRecentActivityItems"); public static final EventId Dashboard_MyDashboardStatistics = new EventId(15001, "Dashboard_MyDashboardStatistics"); public static final EventId Dashboard_PublicDashboardStatistics = new EventId(15002, "Dashboard_PublicDashboardStatistics"); - + + public static final EventId Notification_Persist = new EventId(16000, "Notification_Persist"); + + public static final EventId Lock_Query = new EventId(17000, "Lock_Query"); + public static final EventId Lock_Lookup = new EventId(17001, "Lock_Lookup"); + public static final EventId Lock_Persist = new EventId(17002, "Lock_Persist"); + public static final EventId Lock_Delete = new EventId(17003, "Lock_Delete"); } 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 13232020c..85558dca5 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 @@ -10,9 +10,9 @@ public final class Permission { public static String AuthenticatedRole = "AuthenticatedRole"; public static String PublicRole = "PublicRole"; public static String DatasetProfileManagerRole = "DatasetProfileManagerRole"; - + ///// - + //Public public static String PublicBrowseDescription = "PublicBrowseDescription"; public static String PublicBrowseDescriptionTemplate = "BrowseDescriptionTemplate"; @@ -22,12 +22,12 @@ public final class Permission { public static String PublicBrowseReference = "PublicBrowseReference"; public static String PublicBrowseUser = "PublicBrowseUser"; public static String PublicBrowseDashboardStatistics = "PublicBrowseDashboardStatistics"; - + //Elastic public static String ManageElastic = "ManageElastic"; - + //Language public static String BrowseLanguage = "BrowseLanguage"; public static String EditLanguage = "EditLanguage"; @@ -60,7 +60,7 @@ public final class Permission { public static String BrowseStorageFile = "BrowseStorageFile"; public static String EditStorageFile = "EditStorageFile"; public static String DeleteStorageFile = "DeleteStorageFile"; - + //DescriptionTemplateType public static String BrowseDescriptionTemplateType = "BrowseDescriptionTemplateType"; public static String EditDescriptionTemplateType = "EditDescriptionTemplateType"; @@ -161,5 +161,10 @@ public final class Permission { public static String EditTenantUser = "EditTenantUser"; public static String DeleteTenantUser = "DeleteTenantUser"; + //Lock + public static String BrowseLock = "BrowseLock"; + public static String EditLock = "EditLock"; + public static String DeleteLock = "DeleteLock"; + } diff --git a/dmp-backend/core/src/main/java/eu/eudat/commons/enums/LockTargetType.java b/dmp-backend/core/src/main/java/eu/eudat/commons/enums/LockTargetType.java new file mode 100644 index 000000000..4b548c10f --- /dev/null +++ b/dmp-backend/core/src/main/java/eu/eudat/commons/enums/LockTargetType.java @@ -0,0 +1,27 @@ +package eu.eudat.commons.enums; + +import com.fasterxml.jackson.annotation.JsonValue; +import eu.eudat.data.converters.enums.DatabaseEnum; + +import java.util.Map; + +public enum LockTargetType implements DatabaseEnum { + Dmp((short) 0), + Decription((short) 1); + private final Short value; + + LockTargetType(Short value) { + this.value = value; + } + + @JsonValue + public Short getValue() { + return value; + } + + private static final Map map = EnumUtils.getEnumValueMap(LockTargetType.class); + + public static LockTargetType of(Short i) { + return map.get(i); + } +} diff --git a/dmp-backend/core/src/main/java/eu/eudat/data/LockEntity.java b/dmp-backend/core/src/main/java/eu/eudat/data/LockEntity.java new file mode 100644 index 000000000..751198891 --- /dev/null +++ b/dmp-backend/core/src/main/java/eu/eudat/data/LockEntity.java @@ -0,0 +1,90 @@ +package eu.eudat.data; + +import eu.eudat.commons.enums.LockTargetType; +import eu.eudat.data.converters.enums.LockTargetTypeConverter; +import eu.eudat.data.tenant.TenantScopedBaseEntity; +import jakarta.persistence.*; + +import java.time.Instant; +import java.util.UUID; + +@Entity +@Table(name = "\"Lock\"") +public class LockEntity extends TenantScopedBaseEntity { + @Id + @Column(name = "id", columnDefinition = "uuid", updatable = false, nullable = false) + private UUID id; + public static final String _id = "id"; + + @Column(name = "target", columnDefinition = "uuid", updatable = false, nullable = false) + private UUID target; + public static final String _target = "target"; + + @Column(name = "target_type", nullable = false) + @Convert(converter = LockTargetTypeConverter.class) + private LockTargetType targetType; + public static final String _targetType = "targetType"; + + + @Column(name = "locked_by", columnDefinition = "uuid", updatable = false, nullable = false) + private UUID lockedBy; + public static final String _lockedBy = "lockedBy"; + + + @Column(name = "locked_at", nullable = false) + private Instant lockedAt = null; + public static final String _lockedAt = "lockedAt"; + + @Column(name = "touched_at", nullable = true) + private Instant touchedAt; + public static final String _touchedAt = "touchedAt"; + + + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } + + public UUID getTarget() { + return target; + } + + public void setTarget(UUID target) { + this.target = target; + } + + public LockTargetType getTargetType() { + return targetType; + } + + public void setTargetType(LockTargetType targetType) { + this.targetType = targetType; + } + + public UUID getLockedBy() { + return lockedBy; + } + + public void setLockedBy(UUID lockedBy) { + this.lockedBy = lockedBy; + } + + public Instant getLockedAt() { + return lockedAt; + } + + public void setLockedAt(Instant lockedAt) { + this.lockedAt = lockedAt; + } + + public Instant getTouchedAt() { + return touchedAt; + } + + public void setTouchedAt(Instant touchedAt) { + this.touchedAt = touchedAt; + } +} diff --git a/dmp-backend/core/src/main/java/eu/eudat/data/converters/enums/LockTargetTypeConverter.java b/dmp-backend/core/src/main/java/eu/eudat/data/converters/enums/LockTargetTypeConverter.java new file mode 100644 index 000000000..52cbe36b9 --- /dev/null +++ b/dmp-backend/core/src/main/java/eu/eudat/data/converters/enums/LockTargetTypeConverter.java @@ -0,0 +1,11 @@ +package eu.eudat.data.converters.enums; + +import eu.eudat.commons.enums.LockTargetType; +import jakarta.persistence.Converter; + +@Converter +public class LockTargetTypeConverter extends DatabaseEnumConverter{ + protected LockTargetType of(Short i) { + return LockTargetType.of(i); + } +} diff --git a/dmp-backend/core/src/main/java/eu/eudat/model/Lock.java b/dmp-backend/core/src/main/java/eu/eudat/model/Lock.java new file mode 100644 index 000000000..09e163094 --- /dev/null +++ b/dmp-backend/core/src/main/java/eu/eudat/model/Lock.java @@ -0,0 +1,98 @@ +package eu.eudat.model; + +import eu.eudat.commons.enums.LockTargetType; + +import java.time.Instant; +import java.util.UUID; + +public class Lock { + + private UUID id; + public static final String _id = "id"; + + private UUID target; + public static final String _target = "target"; + + private LockTargetType targetType; + public static final String _targetType = "targetType"; + + private User lockedBy; + public static final String _lockedBy = "lockedBy"; + + private Instant lockedAt; + public static final String _lockedAt = "lockedAt"; + + private Instant touchedAt; + public static final String _touchedAt = "touchedAt"; + + private Tenant tenant; + public static final String _tenant = "tenant"; + + private String hash; + + public static final String _hash = "hash"; + + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } + + public UUID getTarget() { + return target; + } + + public void setTarget(UUID target) { + this.target = target; + } + + public LockTargetType getTargetType() { + return targetType; + } + + public void setTargetType(LockTargetType targetType) { + this.targetType = targetType; + } + + public User getLockedBy() { + return lockedBy; + } + + public void setLockedBy(User lockedBy) { + this.lockedBy = lockedBy; + } + + public Instant getLockedAt() { + return lockedAt; + } + + public void setLockedAt(Instant lockedAt) { + this.lockedAt = lockedAt; + } + + public Instant getTouchedAt() { + return touchedAt; + } + + public void setTouchedAt(Instant touchedAt) { + this.touchedAt = touchedAt; + } + + public Tenant getTenant() { + return tenant; + } + + public void setTenant(Tenant tenant) { + this.tenant = tenant; + } + + 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/LockBuilder.java b/dmp-backend/core/src/main/java/eu/eudat/model/builder/LockBuilder.java new file mode 100644 index 000000000..57bcbb236 --- /dev/null +++ b/dmp-backend/core/src/main/java/eu/eudat/model/builder/LockBuilder.java @@ -0,0 +1,140 @@ +package eu.eudat.model.builder; + +import eu.eudat.authorization.AuthorizationFlags; +import eu.eudat.commons.XmlHandlingService; +import eu.eudat.convention.ConventionService; +import eu.eudat.data.LockEntity; +import eu.eudat.model.Lock; +import eu.eudat.model.Tenant; +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 LockBuilder extends BaseBuilder{ + + private final BuilderFactory builderFactory; + private final QueryFactory queryFactory; + private final XmlHandlingService xmlHandlingService; + private EnumSet authorize = EnumSet.of(AuthorizationFlags.None); + + @Autowired + public LockBuilder( + ConventionService conventionService, + BuilderFactory builderFactory, QueryFactory queryFactory, XmlHandlingService xmlHandlingService) { + super(conventionService, new LoggerService(LoggerFactory.getLogger(LockBuilder.class))); + this.builderFactory = builderFactory; + this.queryFactory = queryFactory; + this.xmlHandlingService = xmlHandlingService; + } + + public LockBuilder authorize(EnumSet values) { + this.authorize = values; + return this; + } + + @Override + public List build(FieldSet fields, List data) throws MyApplicationException { + this.logger.debug("building for {} items requesting {} fields", Optional.ofNullable(data).map(List::size).orElse(0), Optional.ofNullable(fields).map(FieldSet::getFields).map(Set::size).orElse(0)); + this.logger.trace(new DataLogEntry("requested fields", fields)); + if (fields == null || data == null || fields.isEmpty()) + return new ArrayList<>(); + + FieldSet userFields = fields.extractPrefixed(this.asPrefix(Lock._lockedBy)); + Map userMap = this.collectUsers(userFields, data); + + FieldSet tenantFields = fields.extractPrefixed(this.asPrefix(Lock._tenant)); + Map tenantMap = this.collectTenants(tenantFields, data); + + List models = new ArrayList<>(); + for (LockEntity d : data) { + Lock m = new Lock(); + if (fields.hasField(this.asIndexer(Lock._id))) m.setId(d.getId()); + if (fields.hasField(this.asIndexer(Lock._target))) m.setTarget(d.getTarget()); + if (fields.hasField(this.asIndexer(Lock._targetType))) m.setTargetType(d.getTargetType()); + if (fields.hasField(this.asIndexer(Lock._lockedAt))) m.setLockedAt(d.getLockedAt()); + if (fields.hasField(this.asIndexer(Lock._touchedAt))) m.setTouchedAt(d.getTouchedAt()); + if (fields.hasField(this.asIndexer(Lock._hash))) m.setHash(this.hashValue(d.getTouchedAt())); + if (!userFields.isEmpty() && userMap != null && userMap.containsKey(d.getLockedBy())) m.setLockedBy(userMap.get(d.getLockedBy())); + if (!tenantFields.isEmpty() && tenantMap != null && tenantMap.containsKey(d.getTenantId())) m.setTenant(tenantMap.get(d.getTenantId())); + models.add(m); + } + this.logger.debug("build {} items", Optional.of(models).map(List::size).orElse(0)); + return models; + } + + private Map collectUsers(FieldSet fields, List data) throws MyApplicationException { + if (fields.isEmpty() || data.isEmpty()) + return null; + this.logger.debug("checking related - {}", User.class.getSimpleName()); + + Map itemMap; + if (!fields.hasOtherField(this.asIndexer(User._id))) { + itemMap = this.asEmpty( + data.stream().map(LockEntity::getLockedBy).distinct().collect(Collectors.toList()), + x -> { + User item = new User(); + item.setId(x); + return item; + }, + User::getId); + } else { + FieldSet clone = new BaseFieldSet(fields.getFields()).ensure(User._id); + UserQuery q = this.queryFactory.query(UserQuery.class).authorize(this.authorize).ids(data.stream().map(LockEntity::getLockedBy).distinct().collect(Collectors.toList())); + itemMap = this.builderFactory.builder(UserBuilder.class).authorize(this.authorize).asForeignKey(q, clone, User::getId); + } + if (!fields.hasField(User._id)) { + itemMap.forEach((id, item) -> { + if (item != null) + item.setId(null); + }); + } + + 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/LockCensor.java b/dmp-backend/core/src/main/java/eu/eudat/model/censorship/LockCensor.java new file mode 100644 index 000000000..f845569fd --- /dev/null +++ b/dmp-backend/core/src/main/java/eu/eudat/model/censorship/LockCensor.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.Lock; +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 LockCensor extends BaseCensor { + private static final LoggerService logger = new LoggerService(LoggerFactory.getLogger(LockCensor.class)); + + protected final AuthorizationService authService; + protected final CensorFactory censorFactory; + + @Autowired + public LockCensor( + 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.BrowseLock); + FieldSet tenantFields = fields.extractPrefixed(this.asIndexerPrefix(Lock._tenant)); + this.censorFactory.censor(TenantCensor.class).censor(tenantFields, null); + FieldSet userFields = fields.extractPrefixed(this.asIndexerPrefix(Lock._lockedBy)); + this.censorFactory.censor(UserCensor.class).censor(userFields, userId); + } +} diff --git a/dmp-backend/core/src/main/java/eu/eudat/model/deleter/LockDeleter.java b/dmp-backend/core/src/main/java/eu/eudat/model/deleter/LockDeleter.java new file mode 100644 index 000000000..c011712a2 --- /dev/null +++ b/dmp-backend/core/src/main/java/eu/eudat/model/deleter/LockDeleter.java @@ -0,0 +1,71 @@ +package eu.eudat.model.deleter; + +import eu.eudat.data.LockEntity; +import eu.eudat.query.LockQuery; +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 jakarta.persistence.EntityManager; +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.util.List; +import java.util.Optional; +import java.util.UUID; + +@Component +@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE) +public class LockDeleter implements Deleter { + + private static final LoggerService logger = new LoggerService(LoggerFactory.getLogger(LockDeleter.class)); + private final EntityManager entityManager; + + protected final QueryFactory queryFactory; + + protected final DeleterFactory deleterFactory; + + @Autowired + public LockDeleter( + EntityManager 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(List::size).orElse(0)).And("ids", ids)); + List data = this.queryFactory.query(LockQuery.class).ids(ids).collect(); + logger.trace("retrieved {} items", Optional.ofNullable(data).map(List::size).orElse(0)); + this.deleteAndSave(data); + } + + public void deleteAndSave(List data) throws InvalidApplicationException { + logger.debug("will delete {} items", Optional.ofNullable(data).map(List::size).orElse(0)); + this.delete(data); + logger.trace("saving changes"); + this.entityManager.flush(); + logger.trace("changes saved"); + } + + public void delete(List data) throws InvalidApplicationException { + logger.debug("will delete {} items", Optional.ofNullable(data).map(List::size).orElse(0)); + if (data == null || data.isEmpty()) + return; + + for (LockEntity item : data) { + logger.trace("deleting item {}", item.getId()); + this.entityManager.remove(item); + logger.trace("removed item"); + } + } + +} diff --git a/dmp-backend/core/src/main/java/eu/eudat/model/deleter/UserDeleter.java b/dmp-backend/core/src/main/java/eu/eudat/model/deleter/UserDeleter.java index 158b48dbc..97f4d5667 100644 --- a/dmp-backend/core/src/main/java/eu/eudat/model/deleter/UserDeleter.java +++ b/dmp-backend/core/src/main/java/eu/eudat/model/deleter/UserDeleter.java @@ -82,6 +82,12 @@ public class UserDeleter implements Deleter { UserContactInfoDeleter deleter = this.deleterFactory.deleter(UserContactInfoDeleter.class); deleter.delete(items); } + { + logger.debug("checking related - {}", TenantUserEntity.class.getSimpleName()); + List items = this.queryFactory.query(TenantUserQuery.class).userIds(ids).collect(); + TenantUserDeleter deleter = this.deleterFactory.deleter(TenantUserDeleter.class); + deleter.delete(items); + } // { // logger.debug("checking related - {}", DmpUserEntity.class.getSimpleName()); // List items = this.queryFactory.query(DmpUserQuery.class).userIds(ids).collect(); diff --git a/dmp-backend/core/src/main/java/eu/eudat/model/persist/LockPersist.java b/dmp-backend/core/src/main/java/eu/eudat/model/persist/LockPersist.java new file mode 100644 index 000000000..1bbed6894 --- /dev/null +++ b/dmp-backend/core/src/main/java/eu/eudat/model/persist/LockPersist.java @@ -0,0 +1,71 @@ +package eu.eudat.model.persist; + +import eu.eudat.commons.enums.LockTargetType; +import eu.eudat.commons.validation.FieldNotNullIfOtherSet; +import eu.eudat.commons.validation.ValidEnum; +import eu.eudat.commons.validation.ValidId; +import jakarta.validation.Valid; + +import java.util.UUID; + +@FieldNotNullIfOtherSet(message = "{validation.hashempty}") +public class LockPersist { + + @ValidId(message = "{validation.invalidid}") + private UUID id; + + @Valid + private UUID target; + + @ValidEnum(message = "{validation.empty}") + private LockTargetType targetType; + + @Valid + private UUID lockedBy; + + public static final String _touchedAt = "touchedAt"; + + private String hash; + + public static final String _hash = "hash"; + + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } + + public UUID getTarget() { + return target; + } + + public void setTarget(UUID target) { + this.target = target; + } + + public LockTargetType getTargetType() { + return targetType; + } + + public void setTargetType(LockTargetType targetType) { + this.targetType = targetType; + } + + public UUID getLockedBy() { + return lockedBy; + } + + public void setLockedBy(UUID lockedBy) { + this.lockedBy = lockedBy; + } + + public String getHash() { + return hash; + } + + public void setHash(String hash) { + this.hash = hash; + } +} diff --git a/dmp-backend/core/src/main/java/eu/eudat/query/LockQuery.java b/dmp-backend/core/src/main/java/eu/eudat/query/LockQuery.java new file mode 100644 index 000000000..d78d6ff2f --- /dev/null +++ b/dmp-backend/core/src/main/java/eu/eudat/query/LockQuery.java @@ -0,0 +1,173 @@ +package eu.eudat.query; + +import eu.eudat.authorization.AuthorizationFlags; +import eu.eudat.commons.enums.LockTargetType; +import eu.eudat.data.LockEntity; +import eu.eudat.model.Lock; +import gr.cite.tools.data.query.FieldResolver; +import gr.cite.tools.data.query.QueryBase; +import gr.cite.tools.data.query.QueryContext; +import jakarta.persistence.Tuple; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.Predicate; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; + +import java.time.Instant; +import java.util.*; + +@Component +@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE) +public class LockQuery extends QueryBase { + + private Collection ids; + + private Collection targetIds; + + private Collection targetTypes; + + private Collection excludedIds; + + private EnumSet authorize = EnumSet.of(AuthorizationFlags.None); + + public LockQuery ids(UUID value) { + this.ids = List.of(value); + return this; + } + + public LockQuery ids(UUID... value) { + this.ids = Arrays.asList(value); + return this; + } + + public LockQuery ids(Collection values) { + this.ids = values; + return this; + } + + public LockQuery targetIds(UUID value) { + this.targetIds = List.of(value); + return this; + } + + public LockQuery targetIds(UUID... value) { + this.targetIds = Arrays.asList(value); + return this; + } + + public LockQuery targetIds(Collection values) { + this.targetIds = values; + return this; + } + + public LockQuery targetTypes(LockTargetType value) { + this.targetTypes = List.of(value); + return this; + } + + public LockQuery targetTypes(LockTargetType... value) { + this.targetTypes = Arrays.asList(value); + return this; + } + + public LockQuery targetTypes(Collection values) { + this.targetTypes = values; + return this; + } + + public LockQuery excludedIds(Collection values) { + this.excludedIds = values; + return this; + } + + public LockQuery excludedIds(UUID value) { + this.excludedIds = List.of(value); + return this; + } + + public LockQuery excludedIds(UUID... value) { + this.excludedIds = Arrays.asList(value); + return this; + } + + public LockQuery authorize(EnumSet values) { + this.authorize = values; + return this; + } + + public LockQuery() { + } + + @Override + protected Class entityClass() { + return LockEntity.class; + } + + @Override + protected Boolean isFalseQuery() { + return this.isEmpty(this.ids) || this.isEmpty(this.targetIds) || this.isEmpty(this.excludedIds) || this.isEmpty(this.targetTypes); + } + + @Override + protected Predicate applyFilters(QueryContext queryContext) { + List predicates = new ArrayList<>(); + if (this.ids != null) { + CriteriaBuilder.In inClause = queryContext.CriteriaBuilder.in(queryContext.Root.get(LockEntity._id)); + for (UUID item : this.ids) + inClause.value(item); + predicates.add(inClause); + } + if (this.targetIds != null) { + CriteriaBuilder.In inClause = queryContext.CriteriaBuilder.in(queryContext.Root.get(LockEntity._target)); + for (UUID item : this.targetIds) + inClause.value(item); + predicates.add(inClause); + } + if (this.targetTypes != null) { + CriteriaBuilder.In inClause = queryContext.CriteriaBuilder.in(queryContext.Root.get(LockEntity._targetType)); + for (LockTargetType item : this.targetTypes) + inClause.value(item); + predicates.add(inClause); + } + if (this.excludedIds != null) { + CriteriaBuilder.In notInClause = queryContext.CriteriaBuilder.in(queryContext.Root.get(LockEntity._id)); + for (UUID item : this.excludedIds) + notInClause.value(item); + predicates.add(notInClause.not()); + } + if (!predicates.isEmpty()) { + Predicate[] predicatesArray = predicates.toArray(new Predicate[0]); + return queryContext.CriteriaBuilder.and(predicatesArray); + } else { + return null; + } + } + + @Override + protected LockEntity convert(Tuple tuple, Set columns) { + LockEntity item = new LockEntity(); + item.setId(QueryBase.convertSafe(tuple, columns, LockEntity._id, UUID.class)); + item.setTarget(QueryBase.convertSafe(tuple, columns, LockEntity._target, UUID.class)); + item.setTargetType(QueryBase.convertSafe(tuple, columns, LockEntity._targetType, LockTargetType.class)); + item.setLockedBy(QueryBase.convertSafe(tuple, columns, LockEntity._lockedBy, UUID.class)); + item.setLockedAt(QueryBase.convertSafe(tuple, columns, LockEntity._lockedAt, Instant.class)); + item.setTouchedAt(QueryBase.convertSafe(tuple, columns, LockEntity._touchedAt, Instant.class)); + item.setTenantId(QueryBase.convertSafe(tuple, columns, LockEntity._tenantId, UUID.class)); + return item; + } + + @Override + protected String fieldNameOf(FieldResolver item) { + if (item.match(Lock._id)) return LockEntity._id; + else if (item.match(Lock._target)) return LockEntity._target; + else if (item.match(Lock._targetType)) return LockEntity._targetType; + else if (item.prefix(Lock._lockedBy)) return LockEntity._lockedBy; + else if (item.match(Lock._lockedAt)) return LockEntity._lockedAt; + else if (item.match(Lock._touchedAt)) return LockEntity._touchedAt; + else if (item.prefix(Lock._tenant)) return LockEntity._tenantId; + else if (item.match(Lock._hash)) return LockEntity._lockedAt; + else return null; + } + +} diff --git a/dmp-backend/core/src/main/java/eu/eudat/query/lookup/LockLookup.java b/dmp-backend/core/src/main/java/eu/eudat/query/lookup/LockLookup.java new file mode 100644 index 000000000..fd5cf7c5d --- /dev/null +++ b/dmp-backend/core/src/main/java/eu/eudat/query/lookup/LockLookup.java @@ -0,0 +1,66 @@ +package eu.eudat.query.lookup; + + +import eu.eudat.commons.enums.LockTargetType; +import eu.eudat.query.LockQuery; +import gr.cite.tools.data.query.Lookup; +import gr.cite.tools.data.query.QueryFactory; + +import java.util.List; +import java.util.UUID; + +public class LockLookup extends Lookup { + + private List ids; + + private List targetIds; + + private List targetTypes; + + private List excludedIds; + + public List getIds() { + return ids; + } + + public void setIds(List ids) { + this.ids = ids; + } + + public List getTargetIds() { + return targetIds; + } + + public void setTargetIds(List targetIds) { + this.targetIds = targetIds; + } + + public List getExcludedIds() { + return excludedIds; + } + + public void setExcludedIds(List excludeIds) { + this.excludedIds = excludeIds; + } + + public List getTargetTypes() { + return targetTypes; + } + + public void setTargetTypes(List targetTypes) { + this.targetTypes = targetTypes; + } + + public LockQuery enrich(QueryFactory queryFactory) { + LockQuery query = queryFactory.query(LockQuery.class); + if (this.ids != null) query.ids(this.ids); + if (this.targetIds != null) query.targetIds(this.targetIds); + if (this.targetTypes != null) query.targetTypes(this.targetTypes); + if (this.excludedIds != null) query.excludedIds(this.excludedIds); + + this.enrichCommon(query); + + return query; + } + +} diff --git a/dmp-backend/core/src/main/java/eu/eudat/service/lock/LockService.java b/dmp-backend/core/src/main/java/eu/eudat/service/lock/LockService.java new file mode 100644 index 000000000..48d2591a6 --- /dev/null +++ b/dmp-backend/core/src/main/java/eu/eudat/service/lock/LockService.java @@ -0,0 +1,24 @@ +package eu.eudat.service.lock; + +import eu.eudat.model.Lock; +import eu.eudat.model.persist.LockPersist; +import eu.eudat.query.lookup.LockLookup; +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 javax.management.InvalidApplicationException; +import java.util.UUID; + +public interface LockService { + + Lock persist(LockPersist model, FieldSet fields) throws MyForbiddenException, MyValidationException, MyApplicationException, MyNotFoundException, InvalidApplicationException; + + boolean isLocked(LockLookup lookup) throws InvalidApplicationException; + + void unlock(LockLookup lookup) throws InvalidApplicationException; + + void deleteAndSave(UUID id) throws MyForbiddenException, InvalidApplicationException; +} diff --git a/dmp-backend/core/src/main/java/eu/eudat/service/lock/LockServiceImpl.java b/dmp-backend/core/src/main/java/eu/eudat/service/lock/LockServiceImpl.java new file mode 100644 index 000000000..8d4327940 --- /dev/null +++ b/dmp-backend/core/src/main/java/eu/eudat/service/lock/LockServiceImpl.java @@ -0,0 +1,188 @@ +package eu.eudat.service.lock; + +import eu.eudat.authorization.AuthorizationFlags; +import eu.eudat.authorization.Permission; +import eu.eudat.commons.scope.user.UserScope; +import eu.eudat.convention.ConventionService; +import eu.eudat.data.LockEntity; +import eu.eudat.errorcode.ErrorThesaurusProperties; +import eu.eudat.model.Lock; +import eu.eudat.model.builder.LockBuilder; +import eu.eudat.model.deleter.LockDeleter; +import eu.eudat.model.persist.LockPersist; +import eu.eudat.query.LockQuery; +import eu.eudat.query.lookup.LockLookup; +import gr.cite.commons.web.authz.service.AuthorizationService; +import gr.cite.tools.data.builder.BuilderFactory; +import gr.cite.tools.data.deleter.DeleterFactory; +import gr.cite.tools.data.query.QueryFactory; +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.BaseFieldSet; +import gr.cite.tools.fieldset.FieldSet; +import gr.cite.tools.logging.LoggerService; +import gr.cite.tools.logging.MapLogEntry; +import jakarta.persistence.EntityManager; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.MessageSource; +import org.springframework.context.i18n.LocaleContextHolder; +import org.springframework.stereotype.Service; + +import javax.management.InvalidApplicationException; +import java.time.Instant; +import java.util.Comparator; +import java.util.Date; +import java.util.List; +import java.util.UUID; + +@Service +public class LockServiceImpl implements LockService { + + private static final LoggerService logger = new LoggerService(LoggerFactory.getLogger(LockServiceImpl.class)); + private final Comparator compareByTouchedAt = Comparator.comparing(o -> o.getTouchedAt()); + private final EntityManager entityManager; + private final UserScope userScope; + private final AuthorizationService authorizationService; + private final DeleterFactory deleterFactory; + private final BuilderFactory builderFactory; + private final QueryFactory queryFactory; + private final ConventionService conventionService; + private final MessageSource messageSource; + private final ErrorThesaurusProperties errors; + + @Autowired + public LockServiceImpl( + EntityManager entityManager, + UserScope userScope, + AuthorizationService authorizationService, + DeleterFactory deleterFactory, + BuilderFactory builderFactory, + QueryFactory queryFactory, + ConventionService conventionService, + MessageSource messageSource, + ErrorThesaurusProperties errors) { + this.entityManager = entityManager; + this.userScope = userScope; + this.authorizationService = authorizationService; + this.deleterFactory = deleterFactory; + this.builderFactory = builderFactory; + this.queryFactory = queryFactory; + this.conventionService = conventionService; + this.messageSource = messageSource; + this.errors = errors; + } + + public Lock persist(LockPersist model, FieldSet fields) throws MyForbiddenException, MyValidationException, MyApplicationException, MyNotFoundException, InvalidApplicationException { + logger.debug(new MapLogEntry("persisting data").And("model", model).And("fields", fields)); + + this.authorizationService.authorizeForce(Permission.EditLock); + + Boolean isUpdate = this.conventionService.isValidGuid(model.getId()); + + LockEntity data; + if (isUpdate) { + data = this.entityManager.find(LockEntity.class, model.getId()); + if (data == null) throw new MyNotFoundException(messageSource.getMessage("General_ItemNotFound", new Object[]{model.getId(), Lock.class.getSimpleName()}, LocaleContextHolder.getLocale())); + if (!data.getLockedBy().equals(this.userScope.getUserId())) throw new MyApplicationException("Is not locked by that user"); + if (!this.conventionService.hashValue(data.getTouchedAt()).equals(model.getHash())) throw new MyValidationException(this.errors.getHashConflict().getCode(), this.errors.getHashConflict().getMessage()); + } else { + data = new LockEntity(); + data.setId(UUID.randomUUID()); + data.setLockedAt(Instant.now()); + data.setLockedBy(this.userScope.getUserId()); + } + + data.setTarget(model.getTarget()); + data.setTargetType(model.getTargetType()); + data.setTouchedAt(Instant.now()); + + if (isUpdate) this.entityManager.merge(data); + else this.entityManager.persist(data); + + this.entityManager.flush(); + + return this.builderFactory.builder(LockBuilder.class).authorize(AuthorizationFlags.OwnerOrDmpAssociatedOrPermissionOrPublic).build(BaseFieldSet.build(fields, Lock._id), data); + } + + public boolean isLocked(LockLookup lookup) throws InvalidApplicationException { + if (lookup !=null && lookup.getTargetIds() != null && lookup.getTargetIds().size() > 0){ + LockQuery query = this.queryFactory.query(LockQuery.class).authorize(AuthorizationFlags.OwnerOrDmpAssociatedOrPermissionOrPublic).targetIds(lookup.getTargetIds()); + if (query.count() == 1) { + LockEntity lock = query.firstAs(lookup.getProject()); + if (lock.getLockedBy().equals(this.userScope.getUserId())) { + lock.setTouchedAt(Instant.now()); + this.entityManager.merge(lock); + this.entityManager.flush(); + return false; + } + return this.forceUnlock(lookup) > 0; + } else if (query.count() > 1) { + this.forceUnlock(lookup); + return this.isLocked(lookup); + } + return false; + } else{ + throw new InvalidApplicationException("Wrong LockLookup"); + } + } + + private Long forceUnlock(LockLookup lookup) throws InvalidApplicationException { + + LockQuery query = this.queryFactory.query(LockQuery.class).authorize(AuthorizationFlags.OwnerOrDmpAssociatedOrPermissionOrPublic).targetIds(lookup.getTargetIds()); + Long availableLocks = query.count(); + long deletedLocks = 0L; + if (availableLocks > 0) { + List locks = query.collectAs(lookup.getProject()); + for (LockEntity lock : locks) { + if (new Date().getTime() - Date.from(lock.getTouchedAt()).getTime() > 120000) { + this.deleteAndSave(lock.getId()); + deletedLocks++; + } + } + if (deletedLocks == 0) { + LockEntity recentLock = locks.stream().max(compareByTouchedAt).get(); + for (LockEntity lock : locks) { + if (lock != recentLock) { + this.deleteAndSave(lock.getId()); + deletedLocks++; + } + } + } + } + return availableLocks - deletedLocks; + } + + public void unlock(LockLookup lookup) throws InvalidApplicationException { + + LockQuery query = this.queryFactory.query(LockQuery.class).authorize(AuthorizationFlags.OwnerOrDmpAssociatedOrPermissionOrPublic).targetIds(lookup.getTargetIds()); + if (query.count() == 1) { + LockEntity lock = query.firstAs(lookup.getProject()); + if (!lock.getLockedBy().equals(this.userScope.getUserId())) { + throw new InvalidApplicationException("Only the user who created that lock can delete it"); + } + this.deleteAndSave(lock.getId()); + } else if (query.count() > 1) { + List locks = query.collectAs(lookup.getProject()); + locks.stream().filter(lock -> lock.getLockedBy().equals(this.userScope.getUserIdSafe())).forEach(lock -> { + try { + this.deleteAndSave(lock.getId()); + } catch (InvalidApplicationException e) { + throw new RuntimeException(e); + } + }); + + } + } + + public void deleteAndSave(UUID id) throws MyForbiddenException, InvalidApplicationException { + logger.debug("deleting : {}", id); + + this.authorizationService.authorizeForce(Permission.DeleteLock); + + this.deleterFactory.deleter(LockDeleter.class).deleteAndSaveByIds(List.of(id)); + } +} + diff --git a/dmp-backend/web/src/main/java/eu/eudat/controllers/LockController.java b/dmp-backend/web/src/main/java/eu/eudat/controllers/LockController.java deleted file mode 100644 index 688073bfb..000000000 --- a/dmp-backend/web/src/main/java/eu/eudat/controllers/LockController.java +++ /dev/null @@ -1,65 +0,0 @@ -package eu.eudat.controllers; - -import eu.eudat.authorization.Permission; -import eu.eudat.logic.managers.LockManager; -import eu.eudat.models.data.helpers.responses.ResponseItem; -import eu.eudat.models.data.lock.Lock; -import eu.eudat.types.ApiMessageCode; -import gr.cite.commons.web.authz.service.AuthorizationService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.bind.annotation.*; - -import java.util.UUID; - -@RestController -@CrossOrigin -@RequestMapping(value = {"/api/lock/"}) -public class LockController { - - private LockManager lockManager; - private final AuthorizationService authorizationService; - - @Autowired - public LockController(LockManager lockManager, AuthorizationService authorizationService) { - this.lockManager = lockManager; - this.authorizationService = authorizationService; - } - - @Transactional - @RequestMapping(method = RequestMethod.GET, path = "target/status/{id}") - public @ResponseBody ResponseEntity> getLocked(@PathVariable String id) throws Exception { - this.authorizationService.authorizeForce(Permission.AuthenticatedRole); - - boolean locked = this.lockManager.isLocked(id); - return ResponseEntity.status(HttpStatus.OK).body(new ResponseItem().status(ApiMessageCode.SUCCESS_MESSAGE).message("locked").payload(locked)); - } - - @Transactional - @RequestMapping(method = RequestMethod.DELETE, path = "target/unlock/{id}") - public @ResponseBody ResponseEntity> unlock(@PathVariable String id) throws Exception { - this.authorizationService.authorizeForce(Permission.AuthenticatedRole); - - this.lockManager.unlock(id); - return ResponseEntity.status(HttpStatus.OK).body(new ResponseItem().status(ApiMessageCode.SUCCESS_MESSAGE).message("Created").payload("Lock Removed")); - } - - @Transactional - @RequestMapping(method = RequestMethod.POST, consumes = "application/json", produces = "application/json") - public @ResponseBody ResponseEntity> createOrUpdate(@RequestBody Lock lock) throws Exception { - this.authorizationService.authorizeForce(Permission.AuthenticatedRole); - - eu.eudat.data.old.Lock result = this.lockManager.createOrUpdate(lock); - return ResponseEntity.status(HttpStatus.OK).body(new ResponseItem().status(ApiMessageCode.SUCCESS_MESSAGE).message("Created").payload(result.getId())); - } - - @RequestMapping(method = RequestMethod.GET, path = "target/{id}") - public @ResponseBody ResponseEntity> getSingle(@PathVariable String id) throws Exception { - this.authorizationService.authorizeForce(Permission.AuthenticatedRole); - - Lock lock = this.lockManager.getFromTarget(id); - return ResponseEntity.status(HttpStatus.OK).body(new ResponseItem().status(ApiMessageCode.NO_MESSAGE).payload(lock)); - } -} diff --git a/dmp-backend/web/src/main/java/eu/eudat/controllers/v2/LockController.java b/dmp-backend/web/src/main/java/eu/eudat/controllers/v2/LockController.java new file mode 100644 index 000000000..67994b553 --- /dev/null +++ b/dmp-backend/web/src/main/java/eu/eudat/controllers/v2/LockController.java @@ -0,0 +1,218 @@ +package eu.eudat.controllers.v2; + +import eu.eudat.audit.AuditableAction; +import eu.eudat.authorization.AuthorizationFlags; +import eu.eudat.authorization.Permission; +import eu.eudat.data.LockEntity; +import eu.eudat.model.Lock; +import eu.eudat.model.builder.LockBuilder; +import eu.eudat.model.censorship.LockCensor; +import eu.eudat.model.persist.LockPersist; +import eu.eudat.model.result.QueryResult; +import eu.eudat.models.data.helpers.responses.ResponseItem; +import eu.eudat.query.LockQuery; +import eu.eudat.query.lookup.LockLookup; +import eu.eudat.service.lock.LockService; +import eu.eudat.types.ApiMessageCode; +import gr.cite.commons.web.authz.service.AuthorizationService; +import gr.cite.tools.auditing.AuditService; +import gr.cite.tools.data.builder.BuilderFactory; +import gr.cite.tools.data.censor.CensorFactory; +import gr.cite.tools.data.query.QueryFactory; +import gr.cite.tools.exception.MyApplicationException; +import gr.cite.tools.exception.MyForbiddenException; +import gr.cite.tools.exception.MyNotFoundException; +import gr.cite.tools.fieldset.FieldSet; +import gr.cite.tools.logging.LoggerService; +import gr.cite.tools.logging.MapLogEntry; +import gr.cite.tools.validation.MyValidate; +import jakarta.transaction.Transactional; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.MessageSource; +import org.springframework.context.i18n.LocaleContextHolder; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import javax.crypto.BadPaddingException; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import javax.management.InvalidApplicationException; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.util.AbstractMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +@RestController +@RequestMapping(path = {"api/lock"}) +public class LockController { + + private static final LoggerService logger = new LoggerService(LoggerFactory.getLogger(LockController.class)); + + private final BuilderFactory builderFactory; + + private final AuditService auditService; + + private final LockService lockService; + + private final CensorFactory censorFactory; + + private final QueryFactory queryFactory; + + private final MessageSource messageSource; + private final AuthorizationService authorizationService; + + @Autowired + public LockController(BuilderFactory builderFactory, + AuditService auditService, + LockService lockService, + CensorFactory censorFactory, + QueryFactory queryFactory, + MessageSource messageSource, + AuthorizationService authorizationService) { + this.builderFactory = builderFactory; + this.auditService = auditService; + this.lockService = lockService; + this.censorFactory = censorFactory; + this.queryFactory = queryFactory; + this.messageSource = messageSource; + this.authorizationService = authorizationService; + } + + @PostMapping("query") + public QueryResult query(@RequestBody LockLookup lookup) throws MyApplicationException, MyForbiddenException, InvalidAlgorithmParameterException, NoSuchPaddingException, IllegalBlockSizeException, NoSuchAlgorithmException, BadPaddingException, InvalidKeyException { + logger.debug("querying {}", Lock.class.getSimpleName()); + + this.censorFactory.censor(LockCensor.class).censor(lookup.getProject(), null); + + LockQuery query = lookup.enrich(this.queryFactory).authorize(AuthorizationFlags.OwnerOrDmpAssociatedOrPermissionOrPublic); + List data = query.collectAs(lookup.getProject()); + List models = this.builderFactory.builder(LockBuilder.class).authorize(AuthorizationFlags.OwnerOrDmpAssociatedOrPermissionOrPublic).build(lookup.getProject(), data); + long count = (lookup.getMetadata() != null && lookup.getMetadata().getCountAll()) ? query.count() : models.size(); + + this.auditService.track(AuditableAction.Lock_Query, "lookup", lookup); + + return new QueryResult(models, count); + } + + @GetMapping("{id}") + public Lock get(@PathVariable("id") UUID id, FieldSet fieldSet) throws MyApplicationException, MyForbiddenException, MyNotFoundException { + logger.debug(new MapLogEntry("retrieving" + Lock.class.getSimpleName()).And("id", id).And("fields", fieldSet)); + + this.censorFactory.censor(LockCensor.class).censor(fieldSet, null); + + LockQuery query = this.queryFactory.query(LockQuery.class).authorize(AuthorizationFlags.OwnerOrDmpAssociatedOrPermissionOrPublic).ids(id); + Lock model = this.builderFactory.builder(LockBuilder.class).authorize(AuthorizationFlags.OwnerOrDmpAssociatedOrPermissionOrPublic).build(fieldSet, query.firstAs(fieldSet)); + if (model == null) + throw new MyNotFoundException(messageSource.getMessage("General_ItemNotFound", new Object[]{id, Lock.class.getSimpleName()}, LocaleContextHolder.getLocale())); + + this.auditService.track(AuditableAction.Lock_Lookup, Map.ofEntries( + new AbstractMap.SimpleEntry("id", id), + new AbstractMap.SimpleEntry("fields", fieldSet) + )); + + return model; + } + + @PostMapping("persist") + @Transactional + public Lock persist(@MyValidate @RequestBody LockPersist model, FieldSet fieldSet) throws MyApplicationException, MyForbiddenException, MyNotFoundException, InvalidApplicationException { + logger.debug(new MapLogEntry("persisting" + Lock.class.getSimpleName()).And("model", model).And("fieldSet", fieldSet)); + this.censorFactory.censor(LockCensor.class).censor(fieldSet, null); + + Lock persisted = this.lockService.persist(model, fieldSet); + + this.auditService.track(AuditableAction.Lock_Persist, Map.ofEntries( + new AbstractMap.SimpleEntry("model", model), + new AbstractMap.SimpleEntry("fields", fieldSet) + )); + + return persisted; + } + + @GetMapping("target/{id}") + public Lock getWithTarget(@PathVariable("id") UUID targetId, FieldSet fieldSet) throws MyApplicationException, MyForbiddenException, MyNotFoundException { + logger.debug(new MapLogEntry("retrieving" + Lock.class.getSimpleName()).And("targetId", targetId).And("fields", fieldSet)); + + this.censorFactory.censor(LockCensor.class).censor(fieldSet, null); + + LockQuery query = this.queryFactory.query(LockQuery.class).authorize(AuthorizationFlags.OwnerOrDmpAssociatedOrPermissionOrPublic).targetIds(targetId); + Lock model = this.builderFactory.builder(LockBuilder.class).authorize(AuthorizationFlags.OwnerOrDmpAssociatedOrPermissionOrPublic).build(fieldSet, query.firstAs(fieldSet)); + if (model == null) + throw new MyNotFoundException(messageSource.getMessage("General_ItemNotFound", new Object[]{targetId, Lock.class.getSimpleName()}, LocaleContextHolder.getLocale())); + + this.auditService.track(AuditableAction.Lock_Lookup, Map.ofEntries( + new AbstractMap.SimpleEntry("targetId", targetId), + new AbstractMap.SimpleEntry("fields", fieldSet) + )); + + return model; + } + + @Transactional + @PostMapping("target/status/{id}") + public @ResponseBody ResponseEntity> getLocked(@RequestBody LockLookup lookup) throws Exception { + this.authorizationService.authorizeForce(Permission.AuthenticatedRole); + + boolean locked = this.lockService.isLocked(lookup); + return ResponseEntity.status(HttpStatus.OK).body(new ResponseItem().status(ApiMessageCode.SUCCESS_MESSAGE).message("locked").payload(locked)); + } + + @Transactional + @PostMapping("target/unlock/{id}") + public @ResponseBody ResponseEntity> unlock(@RequestBody LockLookup lookup) throws Exception { + this.authorizationService.authorizeForce(Permission.AuthenticatedRole); + + this.lockService.unlock(lookup); + return ResponseEntity.status(HttpStatus.OK).body(new ResponseItem().status(ApiMessageCode.SUCCESS_MESSAGE).message("Created").payload("Lock Removed")); + } + + @DeleteMapping("{id}") + @Transactional + public void delete(@PathVariable("id") UUID id) throws MyForbiddenException, InvalidApplicationException { + logger.debug(new MapLogEntry("retrieving" + Lock.class.getSimpleName()).And("id", id)); + + this.lockService.deleteAndSave(id); + + this.auditService.track(AuditableAction.Lock_Delete, "id", id); + } + +// @Transactional +// @RequestMapping(method = RequestMethod.GET, path = "target/status/{id}") +// public @ResponseBody ResponseEntity> getLocked(@PathVariable String id) throws Exception { +// this.authorizationService.authorizeForce(Permission.AuthenticatedRole); +// +// boolean locked = this.lockManager.isLocked(id); +// return ResponseEntity.status(HttpStatus.OK).body(new ResponseItem().status(ApiMessageCode.SUCCESS_MESSAGE).message("locked").payload(locked)); +// } +// +// @Transactional +// @RequestMapping(method = RequestMethod.DELETE, path = "target/unlock/{id}") +// public @ResponseBody ResponseEntity> unlock(@PathVariable String id) throws Exception { +// this.authorizationService.authorizeForce(Permission.AuthenticatedRole); +// +// this.lockManager.unlock(id); +// return ResponseEntity.status(HttpStatus.OK).body(new ResponseItem().status(ApiMessageCode.SUCCESS_MESSAGE).message("Created").payload("Lock Removed")); +// } +// +// @Transactional +// @RequestMapping(method = RequestMethod.POST, consumes = "application/json", produces = "application/json") +// public @ResponseBody ResponseEntity> createOrUpdate(@RequestBody Lock lock) throws Exception { +// this.authorizationService.authorizeForce(Permission.AuthenticatedRole); +// +// eu.eudat.data.old.Lock result = this.lockManager.createOrUpdate(lock); +// return ResponseEntity.status(HttpStatus.OK).body(new ResponseItem().status(ApiMessageCode.SUCCESS_MESSAGE).message("Created").payload(result.getId())); +// } +// +// @RequestMapping(method = RequestMethod.GET, path = "target/{id}") +// public @ResponseBody ResponseEntity> getSingle(@PathVariable String id) throws Exception { +// this.authorizationService.authorizeForce(Permission.AuthenticatedRole); +// +// Lock lock = this.lockManager.getFromTarget(id); +// return ResponseEntity.status(HttpStatus.OK).body(new ResponseItem().status(ApiMessageCode.NO_MESSAGE).payload(lock)); +// } +} diff --git a/dmp-backend/web/src/main/resources/config/permissions.yml b/dmp-backend/web/src/main/resources/config/permissions.yml index 84d4ebdc3..f94abab73 100644 --- a/dmp-backend/web/src/main/resources/config/permissions.yml +++ b/dmp-backend/web/src/main/resources/config/permissions.yml @@ -634,3 +634,24 @@ permissions: clients: [ ] allowAnonymous: false allowAuthenticated: false + + # Lock Permissions + BrowseLock: + roles: + - Admin + clients: [ ] + allowAnonymous: false + allowAuthenticated: false + EditLock: + roles: + - Admin + clients: [ ] + allowAnonymous: false + allowAuthenticated: false + DeleteLock: + roles: + - Admin + claims: [ ] + clients: [ ] + allowAnonymous: false + allowAuthenticated: false diff --git a/dmp-db-scema/updates/00.01.039_drop_old_Lock_and_add_new.sql b/dmp-db-scema/updates/00.01.039_drop_old_Lock_and_add_new.sql new file mode 100644 index 000000000..5ed175e40 --- /dev/null +++ b/dmp-db-scema/updates/00.01.039_drop_old_Lock_and_add_new.sql @@ -0,0 +1,33 @@ +DO $$DECLARE + this_version CONSTANT varchar := '00.01.039'; +BEGIN + PERFORM * FROM "DBVersion" WHERE version = this_version; + IF FOUND THEN RETURN; END IF; + + DROP TABLE public."Lock"; + + CREATE TABLE public."Lock" + ( + id uuid NOT NULL, + target uuid NOT NULL, + target_type smallint NOT NULL, + locked_by uuid NOT NULL, + locked_at timestamp without time zone NOT NULL, + touched_at timestamp without time zone, + tenant uuid, + CONSTRAINT "Lock_pkey" PRIMARY KEY (id), + CONSTRAINT "Lock_lockedby_fkey" FOREIGN KEY (locked_by) + REFERENCES public."User" (id) MATCH SIMPLE + ON UPDATE NO ACTION + ON DELETE NO ACTION + NOT VALID, + CONSTRAINT "Lock_tenant_fkey" FOREIGN KEY (tenant) + REFERENCES public."Tenant" (id) MATCH SIMPLE + ON UPDATE NO ACTION + ON DELETE NO ACTION + NOT VALID + ); + + INSERT INTO public."DBVersion" VALUES ('DMPDB', '00.01.039', '2023-12-08 12:00:00.000000+02', now(), 'Drop old Lock Table and create New Lock table.'); + +END$$; \ No newline at end of file diff --git a/dmp-frontend/src/app/core/common/enum/lock-target-type.ts b/dmp-frontend/src/app/core/common/enum/lock-target-type.ts new file mode 100644 index 000000000..66cfe133e --- /dev/null +++ b/dmp-frontend/src/app/core/common/enum/lock-target-type.ts @@ -0,0 +1,4 @@ +export enum LockTargetType { + Dmp = 0, + Description = 1 +} \ No newline at end of file diff --git a/dmp-frontend/src/app/core/model/lock/lock.model.ts b/dmp-frontend/src/app/core/model/lock/lock.model.ts index 83ff26df8..27c9904ae 100644 --- a/dmp-frontend/src/app/core/model/lock/lock.model.ts +++ b/dmp-frontend/src/app/core/model/lock/lock.model.ts @@ -1,6 +1,9 @@ import { Guid } from '@common/types/guid'; import { UserInfoListingModel } from '../user/user-info-listing'; +import { LockTargetType } from '@app/core/common/enum/lock-target-type'; +import { User } from '../user/user'; +// old model export class LockModel { id: Guid; target: Guid; @@ -16,3 +19,24 @@ export class LockModel { } } + + +export interface Lock{ + id: Guid; + target: Guid; + targetType: LockTargetType; + lockedBy: User; + lockedAt: Date; + touchedAt: Date; + hash: String; +} + + +// Persist +export interface LockPersist{ + id: Guid; + target: Guid; + targetType: LockTargetType; + lockedBy: User; + hash: String; +} diff --git a/dmp-frontend/src/app/core/query/lock.lookup.ts b/dmp-frontend/src/app/core/query/lock.lookup.ts new file mode 100644 index 000000000..2cad0728c --- /dev/null +++ b/dmp-frontend/src/app/core/query/lock.lookup.ts @@ -0,0 +1,21 @@ +import { Lookup } from "@common/model/lookup"; +import { Guid } from "@common/types/guid"; +import { LockTargetType } from "../common/enum/lock-target-type"; + +export class LockLookup extends Lookup implements LockLookup { + ids: Guid[]; + excludedIds: Guid[]; + targetIds: Guid[]; + targetTypes: LockTargetType[]; + + constructor() { + super(); + } +} + +export interface LockLookup { + ids: Guid[]; + excludedIds: Guid[]; + targetIds: Guid[]; + targetTypes: LockTargetType[]; +} \ No newline at end of file diff --git a/dmp-frontend/src/app/core/services/lock/lock.service.ts b/dmp-frontend/src/app/core/services/lock/lock.service.ts index 3404f1cc0..e939cb1ce 100644 --- a/dmp-frontend/src/app/core/services/lock/lock.service.ts +++ b/dmp-frontend/src/app/core/services/lock/lock.service.ts @@ -1,34 +1,76 @@ import { Injectable } from '@angular/core'; import { HttpHeaders, HttpClient } from '@angular/common/http'; -import { BaseHttpService } from '../http/base-http.service'; -import { environment } from 'environments/environment'; -import { Observable } from 'rxjs'; -import { LockModel } from '@app/core/model/lock/lock.model'; +import { Observable, throwError } from 'rxjs'; +import { Lock, LockModel, LockPersist } from '@app/core/model/lock/lock.model'; import { ConfigurationService } from '../configuration/configuration.service'; +import { BaseHttpV2Service } from '../http/base-http-v2.service'; +import { FilterService } from '@common/modules/text-filter/filter-service'; +import { LockLookup } from '@app/core/query/lock.lookup'; +import { QueryResult } from '@common/model/query-result'; +import { catchError } from 'rxjs/operators'; +import { Guid } from '@common/types/guid'; @Injectable() export class LockService { - private actionUrl: string; private headers = new HttpHeaders(); - constructor(private http: BaseHttpService, private httpClient: HttpClient, private configurationService: ConfigurationService) { - this.actionUrl = configurationService.server + 'lock/'; + constructor(private http: BaseHttpV2Service, private configurationService: ConfigurationService, private filterService: FilterService) { } + private get apiBase(): string { return `${this.configurationService.server}lock`; } + + query(q: LockLookup): Observable> { + const url = `${this.apiBase}/query`; + return this.http.post>(url, q).pipe(catchError((error: any) => throwError(error))); + } + + getSingle(id: Guid, reqFields: string[] = []): Observable { + const url = `${this.apiBase}/${id}`; + const options = { params: { f: reqFields } }; + + return this.http + .get(url, options).pipe( + catchError((error: any) => throwError(error))); + } + + persist(item: LockPersist): Observable { + const url = `${this.apiBase}/persist`; + + return this.http + .post(url, item).pipe( + catchError((error: any) => throwError(error))); + } + + delete(id: Guid): Observable { + const url = `${this.apiBase}/${id}`; + + return this.http + .delete(url).pipe( + catchError((error: any) => throwError(error))); + } + + //ToDo change Parameters checkLockStatus(id: string): Observable { - return this.http.get(`${this.actionUrl}target/status/${id}`, { headers: this.headers }); + return this.http.get(`${this.apiBase}/target/status/${id}`, { headers: this.headers }); } + //ToDo change Parameters unlockTarget(id: string): Observable { - return this.http.delete(`${this.actionUrl}target/unlock/${id}`, { headers: this.headers }); + return this.http.delete(`${this.apiBase}/target/unlock/${id}`, { headers: this.headers }); } - getSingle(id: string): Observable { - return this.http.get(`${this.actionUrl}target/${id}`, { headers: this.headers }); + getSingleWithTarget(targetId: Guid, reqFields: string[] = []): Observable { + const url = `${this.apiBase}/target/${targetId}`; + const options = { params: { f: reqFields } }; + + return this.http + .get(url, options).pipe( + catchError((error: any) => throwError(error))); } + //ToDo replace with persist function createOrUpdate(lock: LockModel): Observable { - return this.http.post(`${this.actionUrl}`, lock, { headers: this.headers }); + return this.http.post(`${this.apiBase}`, lock, { headers: this.headers }); } }