diff --git a/registry-integration-tests/src/test/java/org/gbif/registry/ws/it/collections/service/BaseServiceIT.java b/registry-integration-tests/src/test/java/org/gbif/registry/ws/it/collections/service/BaseServiceIT.java index b8239010e..21e4349a8 100644 --- a/registry-integration-tests/src/test/java/org/gbif/registry/ws/it/collections/service/BaseServiceIT.java +++ b/registry-integration-tests/src/test/java/org/gbif/registry/ws/it/collections/service/BaseServiceIT.java @@ -73,6 +73,17 @@ public void setup() { } } + /** + * Resets the Spring Security context with a given principal and user roles. + * + *

This method sets the specified principal (user identifier) and roles + * for the security context, effectively simulating an authenticated user + * in the system for testing or execution purposes. It clears the existing + * security context and creates a new one with the provided principal and roles. + * + * @param principal The principal (typically the username or identifier of the user) to set in the security context. + * @param role One or more {@link UserRole} values representing the roles or authorities to assign to the principal. + */ protected void resetSecurityContext(String principal, UserRole... role) { simplePrincipalProvider.setPrincipal(principal); SecurityContext ctx = SecurityContextHolder.createEmptyContext(); diff --git a/registry-integration-tests/src/test/java/org/gbif/registry/ws/it/collections/service/suggestions/BaseChangeSuggestionServiceIT.java b/registry-integration-tests/src/test/java/org/gbif/registry/ws/it/collections/service/suggestions/BaseChangeSuggestionServiceIT.java index 6ff3fe574..7a63e4bc0 100644 --- a/registry-integration-tests/src/test/java/org/gbif/registry/ws/it/collections/service/suggestions/BaseChangeSuggestionServiceIT.java +++ b/registry-integration-tests/src/test/java/org/gbif/registry/ws/it/collections/service/suggestions/BaseChangeSuggestionServiceIT.java @@ -181,6 +181,7 @@ public void updateEntityChangeSuggestionTest() { // suggested changes int numberChanges = updateEntity(entity); address.setCity("city"); + numberChanges++; R suggestion = createEmptyChangeSuggestion(); suggestion.setSuggestedEntity(entity); diff --git a/registry-integration-tests/src/test/java/org/gbif/registry/ws/it/collections/service/suggestions/CollectionChangeSuggestionServiceIT.java b/registry-integration-tests/src/test/java/org/gbif/registry/ws/it/collections/service/suggestions/CollectionChangeSuggestionServiceIT.java index 8bd83475b..48c34ea6d 100644 --- a/registry-integration-tests/src/test/java/org/gbif/registry/ws/it/collections/service/suggestions/CollectionChangeSuggestionServiceIT.java +++ b/registry-integration-tests/src/test/java/org/gbif/registry/ws/it/collections/service/suggestions/CollectionChangeSuggestionServiceIT.java @@ -18,6 +18,8 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import org.gbif.api.model.collections.Address; +import static org.gbif.registry.service.collections.utils.MasterSourceUtils.IH_SYNC_USER; + import org.gbif.api.model.collections.AlternativeCode; import org.gbif.api.model.collections.Collection; import org.gbif.api.model.collections.Contact; @@ -35,7 +37,6 @@ import org.gbif.api.vocabulary.collections.IdType; import org.gbif.api.vocabulary.collections.MasterSourceType; import org.gbif.registry.service.collections.suggestions.CollectionChangeSuggestionService; -import static org.gbif.registry.service.collections.utils.MasterSourceUtils.IH_SYNC_USER; import org.gbif.ws.client.filter.SimplePrincipalProvider; import java.net.URI; diff --git a/registry-integration-tests/src/test/java/org/gbif/registry/ws/it/collections/service/suggestions/InstitutionChangeSuggestionServiceIT.java b/registry-integration-tests/src/test/java/org/gbif/registry/ws/it/collections/service/suggestions/InstitutionChangeSuggestionServiceIT.java index a03aedc47..4e39628b8 100644 --- a/registry-integration-tests/src/test/java/org/gbif/registry/ws/it/collections/service/suggestions/InstitutionChangeSuggestionServiceIT.java +++ b/registry-integration-tests/src/test/java/org/gbif/registry/ws/it/collections/service/suggestions/InstitutionChangeSuggestionServiceIT.java @@ -127,7 +127,7 @@ int updateEntity(Institution entity) { entity.setIdentifiers(Collections.singletonList(new Identifier(IdentifierType.LSID, "test"))); entity.setAlternativeCodes( Collections.singletonList(new AlternativeCode(UUID.randomUUID().toString(), "test"))); - return 5; + return 4; } @Override diff --git a/registry-service/src/main/java/org/gbif/registry/service/collections/suggestions/BaseChangeSuggestionService.java b/registry-service/src/main/java/org/gbif/registry/service/collections/suggestions/BaseChangeSuggestionService.java index dfcd2f3e6..990a9c200 100644 --- a/registry-service/src/main/java/org/gbif/registry/service/collections/suggestions/BaseChangeSuggestionService.java +++ b/registry-service/src/main/java/org/gbif/registry/service/collections/suggestions/BaseChangeSuggestionService.java @@ -13,11 +13,23 @@ */ package org.gbif.registry.service.collections.suggestions; +import static com.google.common.base.Preconditions.checkArgument; +import static org.gbif.registry.security.UserRoles.*; +import static org.gbif.registry.service.collections.utils.MasterSourceUtils.COLLECTION_LOCKABLE_FIELDS; +import static org.gbif.registry.service.collections.utils.MasterSourceUtils.IH_SYNC_USER; +import static org.gbif.registry.service.collections.utils.MasterSourceUtils.hasExternalMasterSource; + import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.base.Strings; -import org.gbif.api.model.collections.Collection; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.ParameterizedType; +import java.math.BigDecimal; +import java.util.*; +import java.util.stream.Collectors; import org.gbif.api.model.collections.*; +import org.gbif.api.model.collections.Collection; import org.gbif.api.model.collections.suggestions.Change; import org.gbif.api.model.collections.suggestions.ChangeSuggestion; import org.gbif.api.model.collections.suggestions.CollectionChangeSuggestion; @@ -36,6 +48,7 @@ import org.gbif.api.service.collections.CrudService; import org.gbif.api.vocabulary.Country; import org.gbif.api.vocabulary.UserRole; +import org.gbif.api.vocabulary.collections.MasterSourceType; import org.gbif.registry.events.EventManager; import org.gbif.registry.events.collections.EventType; import org.gbif.registry.events.collections.SubEntityCollectionEvent; @@ -49,6 +62,8 @@ import org.gbif.registry.persistence.mapper.collections.dto.ChangeSuggestionDto; import org.gbif.registry.security.grscicoll.GrSciCollAuthorizationService; import org.gbif.registry.service.collections.merge.MergeService; +import org.gbif.registry.service.collections.utils.MasterSourceUtils; + import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; @@ -57,18 +72,6 @@ import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; -import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.ParameterizedType; -import java.math.BigDecimal; -import java.util.*; -import java.util.stream.Collectors; - -import static com.google.common.base.Preconditions.checkArgument; -import static org.gbif.registry.security.UserRoles.*; -import static org.gbif.registry.service.collections.utils.MasterSourceUtils.IH_SYNC_USER; -import static org.gbif.registry.service.collections.utils.MasterSourceUtils.hasExternalMasterSource; - public abstract class BaseChangeSuggestionService< T extends CollectionEntity & Taggable & Identifiable & MachineTaggable & Commentable & Contactable @@ -305,15 +308,17 @@ protected ChangeSuggestionDto createMergeSuggestionDto(R changeSuggestion) { public void updateChangeSuggestion(R updatedChangeSuggestion) { ChangeSuggestionDto dto = changeSuggestionMapper.get(updatedChangeSuggestion.getKey()); - //checkArgument( - //updatedChangeSuggestion.getComments().size() > dto.getComments().size(), - //"A comment is required"); + checkArgument( + updatedChangeSuggestion.getComments().size() > dto.getComments().size(), + "A comment is required"); if (dto.getType() == Type.CREATE || dto.getType() == Type.UPDATE) { // we get the current entity from the DB to update the suggested entity with the current state // and minimize the risk of having race conditions R changeSuggestion = dtoToChangeSuggestion(dto); + lockFields(changeSuggestion, updatedChangeSuggestion); + Set newChanges = extractChanges( updatedChangeSuggestion.getSuggestedEntity(), changeSuggestion.getSuggestedEntity()); @@ -860,6 +865,22 @@ private boolean isEmptyAddress(Address address) { && address.getCountry() == null; } + private void lockFields(R entityOld, R entityNew) { + List fieldsToLock; + if (entityOld instanceof CollectionChangeSuggestion + && entityOld.getProposedBy().equals(IH_SYNC_USER)) { + fieldsToLock = COLLECTION_LOCKABLE_FIELDS.get(MasterSourceType.IH); + fieldsToLock.forEach( + f -> { + try { + f.getSetter().invoke(entityNew.getSuggestedEntity(), f.getGetter().invoke(entityOld.getSuggestedEntity())); + } catch (Exception e) { + throw new IllegalStateException("Could not lock field", e); + } + }); + } + } + protected abstract R newEmptyChangeSuggestion(); protected abstract ChangeSuggestionDto createConvertToCollectionSuggestionDto(R changeSuggestion);