From d5f9a21a560b71052bf7314f9926da18126ebc05 Mon Sep 17 00:00:00 2001 From: Florent MILLOT <75525996+flomillot@users.noreply.github.com> Date: Sat, 10 Oct 2026 17:04:48 +0200 Subject: [PATCH 01/12] Make root network applicabilities explicit Signed-off-by: Florent MILLOT <75525996+flomillot@users.noreply.github.com> --- .../service/NetworkModificationService.java | 3 ++- .../service/NetworkModificationTreeService.java | 6 ++++-- .../service/RootNetworkNodeInfoService.java | 2 +- .../study/server/service/RootNetworkService.java | 6 ++++++ .../service/NetworkModificationServiceTest.java | 15 +++++++++++++++ 5 files changed, 28 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/gridsuite/study/server/service/NetworkModificationService.java b/src/main/java/org/gridsuite/study/server/service/NetworkModificationService.java index 5b7230896..f55ad82e3 100644 --- a/src/main/java/org/gridsuite/study/server/service/NetworkModificationService.java +++ b/src/main/java/org/gridsuite/study/server/service/NetworkModificationService.java @@ -589,13 +589,14 @@ private NetworkModificationsResult duplicateModifications(UUID targetGroupUuid, ).getBody(); } - public void duplicateModificationsGroup(UUID sourceGroupUuid, UUID groupUuid, UUID newNodeUuid, UUID studyUuid, String userId) { + public void duplicateModificationsGroup(UUID sourceGroupUuid, UUID groupUuid, UUID newNodeUuid, UUID studyUuid, List rootNetworkTags, String userId) { Objects.requireNonNull(groupUuid); Objects.requireNonNull(sourceGroupUuid); var path = UriComponentsBuilder.fromPath("groups/{uuid}/duplicate") .queryParam(QUERY_PARAM_GROUP_UUID, groupUuid) .queryParam(QUERY_PARAM_NODE_CONTAINER_UUID, newNodeUuid) .queryParam(QUERY_PARAM_STUDY_ROOT_CONTAINER_UUID, studyUuid) + .queryParam(QUERY_PARAM_ROOT_NETWORK_TAGS, rootNetworkTags) .buildAndExpand(sourceGroupUuid) .toUriString(); diff --git a/src/main/java/org/gridsuite/study/server/service/NetworkModificationTreeService.java b/src/main/java/org/gridsuite/study/server/service/NetworkModificationTreeService.java index c8303b920..a828bca22 100644 --- a/src/main/java/org/gridsuite/study/server/service/NetworkModificationTreeService.java +++ b/src/main/java/org/gridsuite/study/server/service/NetworkModificationTreeService.java @@ -248,7 +248,8 @@ private UUID duplicateNode(UUID nodeToCopyUuid, UUID anchorNodeUuid, InsertMode ); // Then we create the modification group and recreate references - networkModificationService.duplicateModificationsGroup(modificationGroupUuid, newGroupUuid, node.getId(), studyUuid, userId); + networkModificationService.duplicateModificationsGroup(modificationGroupUuid, newGroupUuid, node.getId(), studyUuid, + rootNetworkService.getStudyRootNetworkTags(studyUuid), userId); return node.getId(); } @@ -560,7 +561,8 @@ public UUID cloneStudyTree(AbstractNode nodeToDuplicate, UUID nodeParentId, Stud nextParentId = duplicateNode(newStudyEntity, nodeParentId, model, InsertMode.CHILD).getId(); - networkModificationService.duplicateModificationsGroup(modificationGroupToDuplicateId, newModificationGroupId, nextParentId, newStudyEntity.getId(), userId); + networkModificationService.duplicateModificationsGroup(modificationGroupToDuplicateId, newModificationGroupId, nextParentId, newStudyEntity.getId(), + rootNetworkService.getStudyRootNetworkTags(newStudyEntity.getId()), userId); } else { // when cloning studyTree, we don't clone root node // if cloning the whole study, the root node is previously created diff --git a/src/main/java/org/gridsuite/study/server/service/RootNetworkNodeInfoService.java b/src/main/java/org/gridsuite/study/server/service/RootNetworkNodeInfoService.java index c57077df9..1a569a9cb 100644 --- a/src/main/java/org/gridsuite/study/server/service/RootNetworkNodeInfoService.java +++ b/src/main/java/org/gridsuite/study/server/service/RootNetworkNodeInfoService.java @@ -591,7 +591,7 @@ public ModificationApplicationContext getNetworkModificationApplicationContext(U rootNetworkUuid).orElseThrow(() -> new StudyException(NOT_FOUND, ROOT_NETWORK_NOT_FOUND)); String variantId = rootNetworkNodeInfoEntity.getVariantId(); UUID reportUuid = rootNetworkNodeInfoEntity.getModificationReports().get(nodeUuid); - return new ModificationApplicationContext(networkUuid, variantId, reportUuid, nodeUuid, rootNetworkNodeInfoEntity.getRootNetwork().getApplicabilityTag()); + return new ModificationApplicationContext(networkUuid, variantId, reportUuid, nodeUuid, rootNetworkNodeInfoEntity.getRootNetwork().getTag()); } private List getReportUuids(RootNetworkNodeInfo rootNetworkNodeInfo) { diff --git a/src/main/java/org/gridsuite/study/server/service/RootNetworkService.java b/src/main/java/org/gridsuite/study/server/service/RootNetworkService.java index 045916e74..df020d3b0 100644 --- a/src/main/java/org/gridsuite/study/server/service/RootNetworkService.java +++ b/src/main/java/org/gridsuite/study/server/service/RootNetworkService.java @@ -394,6 +394,12 @@ public List getStudyRootNetwork(StudyEntity study) { return study.getRootNetworks(); } + public List getStudyRootNetworkTags(UUID studyUuid) { + return rootNetworkRepository.findAllByStudyId(studyUuid).stream() + .map(RootNetworkEntity::getTag) + .toList(); + } + public List getStudyRootNetworkIds(UUID studyUuid) { return rootNetworkRepository.findAllByStudyId(studyUuid).stream() .map(RootNetworkEntity::getId) diff --git a/src/test/java/org/gridsuite/study/server/service/NetworkModificationServiceTest.java b/src/test/java/org/gridsuite/study/server/service/NetworkModificationServiceTest.java index 03dc64771..0d94a827d 100644 --- a/src/test/java/org/gridsuite/study/server/service/NetworkModificationServiceTest.java +++ b/src/test/java/org/gridsuite/study/server/service/NetworkModificationServiceTest.java @@ -240,6 +240,21 @@ void testDeleteRootNetworkTags() { verifyNoMoreInteractions(restTemplate); } + @Test + void testDuplicateModificationsGroupAssociatesTheRootNetworkTags() { + UUID sourceGroupUuid = UUID.randomUUID(); + UUID groupUuid = UUID.randomUUID(); + UUID nodeUuid = UUID.randomUUID(); + UUID studyUuid = UUID.randomUUID(); + String expectedUrl = NETWORK_MODIFICATION_SERVER_URI + "/v1/groups/" + sourceGroupUuid + "/duplicate" + + "?groupUuid=" + groupUuid + "&nodeContainerUuid=" + nodeUuid + "&studyRootContainerUuid=" + studyUuid + + "&rootNetworkTags=PH1&rootNetworkTags=PH2"; + + networkModificationService.duplicateModificationsGroup(sourceGroupUuid, groupUuid, nodeUuid, studyUuid, List.of("PH1", "PH2"), "userId"); + + verify(restTemplate).exchange(eq(expectedUrl), eq(HttpMethod.POST), org.mockito.ArgumentMatchers.>any(), eq(Void.class)); + } + @Test void testAssertReferencedModificationsAreWritable() { UUID groupUuid = UUID.randomUUID(); From cadab4abd32fa5b8e039c0703ee13d073f5d02de Mon Sep 17 00:00:00 2001 From: Florent MILLOT <75525996+flomillot@users.noreply.github.com> Date: Sat, 10 Oct 2026 17:40:39 +0200 Subject: [PATCH 02/12] Fix the mono root network context tag test Signed-off-by: Florent MILLOT <75525996+flomillot@users.noreply.github.com> --- .../server/rootnetworks/RootNetworkApplicabilityTest.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/test/java/org/gridsuite/study/server/rootnetworks/RootNetworkApplicabilityTest.java b/src/test/java/org/gridsuite/study/server/rootnetworks/RootNetworkApplicabilityTest.java index 34f5ffc44..811f2a6b8 100644 --- a/src/test/java/org/gridsuite/study/server/rootnetworks/RootNetworkApplicabilityTest.java +++ b/src/test/java/org/gridsuite/study/server/rootnetworks/RootNetworkApplicabilityTest.java @@ -214,7 +214,7 @@ void testApplicationContextCarriesRootNetworkTag() { } @Test - void testMonoRootStudyCarriesNoRootNetworkTag() { + void testMonoRootStudyBuildCarriesNoRootNetworkTag() { StudyEntity studyEntity = TestUtils.createDummyStudy(NETWORK_UUID, CASE_UUID, CASE_NAME, CASE_FORMAT, REPORT_UUID); studyEntity.setMonoRoot(true); studyRepository.save(studyEntity); @@ -228,7 +228,9 @@ void testMonoRootStudyCarriesNoRootNetworkTag() { ArgumentCaptor buildInfosCaptor = ArgumentCaptor.captor(); verify(networkModificationService).buildNode(any(UUID.class), any(UUID.class), buildInfosCaptor.capture(), isNull()); assertNull(buildInfosCaptor.getValue().getRootNetworkTag()); - assertNull(rootNetworkNodeInfoService.getNetworkModificationApplicationContext(rootNetworkEntity.getId(), firstNode.getId(), NETWORK_UUID).rootNetworkTag()); + // whereas the modifications written are associated with the real tag, for the study to turn multi root network later + assertEquals(rootNetworkEntity.getTag(), + rootNetworkNodeInfoService.getNetworkModificationApplicationContext(rootNetworkEntity.getId(), firstNode.getId(), NETWORK_UUID).rootNetworkTag()); } @Test From 4bf34443349edcfcc2c432e223e670ab6ef286fb Mon Sep 17 00:00:00 2001 From: Florent MILLOT <75525996+flomillot@users.noreply.github.com> Date: Tue, 13 Oct 2026 20:28:56 +0200 Subject: [PATCH 03/12] Refuse root network tags the shared modifications the user cannot write on miss Signed-off-by: Florent MILLOT <75525996+flomillot@users.noreply.github.com> --- .../server/controller/StudyController.java | 11 +++--- .../dto/SharedModificationsWriteAccess.java | 18 ++++++++++ .../service/NetworkModificationService.java | 23 ++++++++---- .../study/server/service/StudyService.java | 31 +++++++++------- .../RootNetworkApplicabilityTest.java | 35 ++++++++++--------- .../NetworkModificationServiceTest.java | 35 +++++++++++++------ .../server/studycontroller/StudyTestBase.java | 1 + .../study/server/utils/TestUtils.java | 5 +++ .../server/utils/wiremock/WireMockStubs.java | 7 ++++ 9 files changed, 115 insertions(+), 51 deletions(-) create mode 100644 src/main/java/org/gridsuite/study/server/dto/SharedModificationsWriteAccess.java diff --git a/src/main/java/org/gridsuite/study/server/controller/StudyController.java b/src/main/java/org/gridsuite/study/server/controller/StudyController.java index c22a7b131..f424c0ade 100644 --- a/src/main/java/org/gridsuite/study/server/controller/StudyController.java +++ b/src/main/java/org/gridsuite/study/server/controller/StudyController.java @@ -949,11 +949,12 @@ public ResponseEntity> getAvailableSvgComponentLibraries() { return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(libraries); } - @GetMapping(value = "/studies/{studyUuid}/network-modifications/references/exists") - @Operation(summary = "Whether the study contains a shared modification, including nested in its composites") - @ApiResponse(responseCode = "200", description = "true if at least one shared modification is found") - public ResponseEntity hasSharedModifications(@PathVariable("studyUuid") UUID studyUuid) { - return ResponseEntity.ok().body(studyService.hasSharedModifications(studyUuid)); + @GetMapping(value = "/studies/{studyUuid}/network-modifications/references/write-access") + @Operation(summary = "Whether the study contains shared modifications, including nested in its composites, and, if so, whether the user can write on all of them") + @ApiResponse(responseCode = "200", description = "The write access on the shared modifications was returned") + public ResponseEntity getSharedModificationsWriteAccess(@PathVariable("studyUuid") UUID studyUuid, + @RequestHeader(HEADER_USER_ID) String userId) { + return ResponseEntity.ok().body(studyService.getSharedModificationsWriteAccess(studyUuid, userId)); } @GetMapping(value = "/studies/{studyUuid}/nodes/{nodeUuid}/network-modifications", produces = MediaType.TEXT_PLAIN_VALUE) diff --git a/src/main/java/org/gridsuite/study/server/dto/SharedModificationsWriteAccess.java b/src/main/java/org/gridsuite/study/server/dto/SharedModificationsWriteAccess.java new file mode 100644 index 000000000..d57a90cbd --- /dev/null +++ b/src/main/java/org/gridsuite/study/server/dto/SharedModificationsWriteAccess.java @@ -0,0 +1,18 @@ +/** + * Copyright (c) 2026, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.gridsuite.study.server.dto; + +/** + * Whether a user can write on the shared modifications of a study. + * + * @author Florent MILLOT + */ +public enum SharedModificationsWriteAccess { + NO_SHARED_MODIFICATION, + ALL_WRITABLE, + SOME_NOT_WRITABLE +} diff --git a/src/main/java/org/gridsuite/study/server/service/NetworkModificationService.java b/src/main/java/org/gridsuite/study/server/service/NetworkModificationService.java index f55ad82e3..52f205bb8 100644 --- a/src/main/java/org/gridsuite/study/server/service/NetworkModificationService.java +++ b/src/main/java/org/gridsuite/study/server/service/NetworkModificationService.java @@ -14,6 +14,7 @@ import org.gridsuite.study.server.dto.BuildInfos; import org.gridsuite.study.server.dto.ModificationReference; import org.gridsuite.study.server.dto.NodeReceiver; +import org.gridsuite.study.server.dto.SharedModificationsWriteAccess; import org.gridsuite.study.server.dto.modification.*; import org.gridsuite.study.server.dto.workflow.AbstractWorkflowInfos; import org.springframework.beans.factory.annotation.Autowired; @@ -405,7 +406,7 @@ public void updateRootNetworkApplicability(List modificationsUuids, String /** * Renames a root network tag in the applicabilities held by the modifications of the given groups. */ - public void renameRootNetworkTag(List groupUuids, String oldTag, String newTag) { + public void renameRootNetworkTag(List groupUuids, String oldTag, String newTag, String userId) { Objects.requireNonNull(oldTag); Objects.requireNonNull(newTag); if (CollectionUtils.isEmpty(groupUuids)) { @@ -421,6 +422,7 @@ public void renameRootNetworkTag(List groupUuids, String oldTag, String ne HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); + headers.set(HEADER_USER_ID, userId); restTemplate.exchange(path, HttpMethod.PUT, new HttpEntity<>(headers), Void.class); } @@ -743,16 +745,25 @@ public boolean hasModificationReferences(List containerUuids) { return Boolean.TRUE.equals(restTemplate.getForObject(getNetworkModificationServerURI(false) + path, Boolean.class)); } + public SharedModificationsWriteAccess getReferencedModificationsWriteAccess(List containerUuids, String userId) { + String path = UriComponentsBuilder.fromPath(CONTAINERS + DELIMITER + "references/write-access") + .queryParam(UUIDS, containerUuids) + .build().toUriString(); + return restTemplate.exchange(getNetworkModificationServerURI(false) + path, HttpMethod.GET, + userIdEntity(userId), SharedModificationsWriteAccess.class).getBody(); + } + /** - * Asserts that the user may write on every shared modification the given containers point to, directly or through - * other shared modifications. Throws when they cannot. + * Asserts that the shared modifications the user cannot write on, among the ones the given containers point to, are + * all associated with the given root network tags. Throws a forbidden error when they are not. */ - public void assertReferencedModificationsAreWritable(List containerUuids, String userId) { - if (containerUuids.isEmpty()) { + public void assertRootNetworkTagsAllowed(List containerUuids, List rootNetworkTags, String userId) { + if (rootNetworkTags.isEmpty() || containerUuids.isEmpty()) { return; } - String path = UriComponentsBuilder.fromPath(CONTAINERS + DELIMITER + "references/authorized") + String path = UriComponentsBuilder.fromPath(CONTAINERS + DELIMITER + "root-network-tags/authorized") .queryParam(UUIDS, containerUuids) + .queryParam("tags", rootNetworkTags) .build().toUriString(); restTemplate.exchange(getNetworkModificationServerURI(false) + path, HttpMethod.GET, userIdEntity(userId), Void.class); diff --git a/src/main/java/org/gridsuite/study/server/service/StudyService.java b/src/main/java/org/gridsuite/study/server/service/StudyService.java index 1a651c2c3..66be7f35e 100644 --- a/src/main/java/org/gridsuite/study/server/service/StudyService.java +++ b/src/main/java/org/gridsuite/study/server/service/StudyService.java @@ -337,6 +337,9 @@ public void deleteRootNetworks(UUID studyUuid, List rootNetworksUuids, Str @Transactional public RootNetworkRequestInfos createRootNetworkRequest(UUID studyUuid, RootNetworkInfos rootNetworkInfos, String userId) { rootNetworkService.assertCanCreateRootNetwork(studyUuid, rootNetworkInfos.getName(), rootNetworkInfos.getTag()); + if (rootNetworkInfos.getTag() != null) { + assertCanAddRootNetworkTag(studyUuid, rootNetworkInfos.getTag(), userId); + } StudyEntity studyEntity = getStudy(studyUuid); rootNetworkInfos.setId(UUID.randomUUID()); @@ -393,33 +396,35 @@ private void invalidatePreviousRootNetworkNodeTree(StudyEntity studyEntity) { invalidateNodeTree(studyEntity.getId(), rootNodeUuid, studyEntity.getFirstRootNetwork().getId()); } - private void updateRootNetworkBasicInfos(UUID studyUuid, RootNetworkInfos rootNetworkInfos, boolean updateCase) { + private void updateRootNetworkBasicInfos(UUID studyUuid, RootNetworkInfos rootNetworkInfos, boolean updateCase, String userId) { String previousTag = rootNetworkService.getRootNetworkTag(rootNetworkInfos.getId()); rootNetworkService.updateRootNetwork(rootNetworkInfos, updateCase); // the entity says what the tag really became, unlike the DTO String newTag = rootNetworkService.getRootNetworkTag(rootNetworkInfos.getId()); - renameRootNetworkTagInApplicabilities(studyUuid, previousTag, newTag); + renameRootNetworkTagInApplicabilities(studyUuid, previousTag, newTag, userId); postRootNetworkUpdate(studyUuid, rootNetworkInfos.getId(), updateCase); } - private void renameRootNetworkTagInApplicabilities(UUID studyUuid, String previousTag, String newTag) { + private void renameRootNetworkTagInApplicabilities(UUID studyUuid, String previousTag, String newTag, String userId) { if (previousTag == null || previousTag.equals(newTag)) { return; } - networkModificationService.renameRootNetworkTag(getStudyModificationGroupUuids(studyUuid), previousTag, newTag); + networkModificationService.renameRootNetworkTag(getStudyModificationGroupUuids(studyUuid), previousTag, newTag, userId); } /** - * Renaming a tag rewrites the applicabilities the shared modifications of the study contain for every study - * referencing them: only a user allowed to write on all of them may do it. + * A root network tag of a study must be associated with all its shared modifications: the ones the user cannot + * write on, which they cannot associate with a new tag, must already be. */ - private void assertCanRenameRootNetworkTag(UUID studyUuid, String userId) { - networkModificationService.assertReferencedModificationsAreWritable(getStudyModificationGroupUuids(studyUuid), userId); + private void assertCanAddRootNetworkTag(UUID studyUuid, String newTag, String userId) { + networkModificationService.assertRootNetworkTagsAllowed(getStudyModificationGroupUuids(studyUuid), List.of(newTag), userId); } - public boolean hasSharedModifications(UUID studyUuid) { + public SharedModificationsWriteAccess getSharedModificationsWriteAccess(UUID studyUuid, String userId) { List groupUuids = getStudyModificationGroupUuids(studyUuid); - return !groupUuids.isEmpty() && networkModificationService.hasModificationReferences(groupUuids); + return groupUuids.isEmpty() + ? SharedModificationsWriteAccess.NO_SHARED_MODIFICATION + : networkModificationService.getReferencedModificationsWriteAccess(groupUuids, userId); } private List getStudyModificationGroupUuids(UUID studyUuid) { @@ -433,7 +438,7 @@ public void updateRootNetworkRequest(UUID studyUuid, RootNetworkInfos rootNetwor rootNetworkService.assertCanModifyRootNetwork(studyUuid, rootNetworkInfos.getId(), rootNetworkInfos.getName(), rootNetworkInfos.getTag()); String newTag = rootNetworkInfos.getTag(); if (newTag != null && !newTag.equals(rootNetworkService.getRootNetworkTag(rootNetworkInfos.getId()))) { - assertCanRenameRootNetworkTag(studyUuid, userId); + assertCanAddRootNetworkTag(studyUuid, newTag, userId); } StudyEntity studyEntity = getStudy(studyUuid); @@ -441,7 +446,7 @@ public void updateRootNetworkRequest(UUID studyUuid, RootNetworkInfos rootNetwor RootNetworkRequestEntity requestEntity = rootNetworkService.insertModificationRequest(studyEntity.getId(), rootNetworkInfos, userId); updateRootNetworkCaseInfos(studyEntity.getId(), rootNetworkInfos, userId, requestEntity); } else { - updateRootNetworkBasicInfos(studyEntity.getId(), rootNetworkInfos, false); + updateRootNetworkBasicInfos(studyEntity.getId(), rootNetworkInfos, false, userId); } notificationService.emitElementUpdated(studyUuid, userId); } @@ -460,7 +465,7 @@ private void updateRootNetworkCaseInfos(UUID studyUuid, RootNetworkInfos rootNet @Transactional public void modifyRootNetwork(UUID studyUuid, RootNetworkInfos rootNetworkInfos, String userId) { invalidateStudyRootNetwork(studyUuid, rootNetworkInfos.getId(), userId, true); - updateRootNetworkBasicInfos(studyUuid, rootNetworkInfos, true); + updateRootNetworkBasicInfos(studyUuid, rootNetworkInfos, true, userId); } private void postRootNetworkUpdate(UUID studyUuid, UUID rootNetworkUuid, boolean updateCase) { diff --git a/src/test/java/org/gridsuite/study/server/rootnetworks/RootNetworkApplicabilityTest.java b/src/test/java/org/gridsuite/study/server/rootnetworks/RootNetworkApplicabilityTest.java index 811f2a6b8..8d6663bab 100644 --- a/src/test/java/org/gridsuite/study/server/rootnetworks/RootNetworkApplicabilityTest.java +++ b/src/test/java/org/gridsuite/study/server/rootnetworks/RootNetworkApplicabilityTest.java @@ -39,6 +39,7 @@ import java.util.*; +import static org.gridsuite.study.server.StudyConstants.HEADER_USER_ID; import static org.gridsuite.study.server.utils.TestUtils.createModificationNodeInfo; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -275,20 +276,20 @@ void testRenamingARootNetworkTagCarriesTheApplicabilitiesAlong() throws Exceptio updateRootNetwork(studyEntity.getId(), rootNetworkUuid, ROOT_NETWORK_TAG_2); ArgumentCaptor> groupUuidsCaptor = ArgumentCaptor.captor(); - verify(networkModificationService, times(1)).renameRootNetworkTag(groupUuidsCaptor.capture(), eq(ROOT_NETWORK_TAG_1), eq(ROOT_NETWORK_TAG_2)); + verify(networkModificationService, times(1)).renameRootNetworkTag(groupUuidsCaptor.capture(), eq(ROOT_NETWORK_TAG_1), eq(ROOT_NETWORK_TAG_2), eq(USER_ID)); assertEquals(Set.of(firstNode.getModificationGroupUuid(), secondNode.getModificationGroupUuid()), Set.copyOf(groupUuidsCaptor.getValue())); // an update carrying no tag at all leaves it as it was, so it is not a rename updateRootNetwork(studyEntity.getId(), rootNetworkUuid, null); - verify(networkModificationService, times(1)).renameRootNetworkTag(any(), any(), any()); + verify(networkModificationService, times(1)).renameRootNetworkTag(any(), any(), any(), any()); // and neither is an update carrying the tag the root network already has updateRootNetwork(studyEntity.getId(), rootNetworkUuid, ROOT_NETWORK_TAG_2); - verify(networkModificationService, times(1)).renameRootNetworkTag(any(), any(), any()); + verify(networkModificationService, times(1)).renameRootNetworkTag(any(), any(), any(), any()); } @Test - void testRenamingARootNetworkTagNeedsWritePermissionOnTheSharedModifications() throws Exception { + void testRenamingARootNetworkTagNeedsTheSharedModificationsTheUserCannotWriteOnToBeAssociatedWithIt() throws Exception { StudyEntity studyEntity = TestUtils.createDummyStudy(NETWORK_UUID, CASE_UUID, CASE_NAME, CASE_FORMAT, REPORT_UUID); studyRepository.save(studyEntity); UUID rootNetworkUuid = studyService.getExistingBasicRootNetworkInfos(studyEntity.getId()).getFirst().rootNetworkUuid(); @@ -296,9 +297,9 @@ void testRenamingARootNetworkTagNeedsWritePermissionOnTheSharedModifications() t NodeEntity rootNode = networkModificationTreeService.createRoot(studyEntity); NetworkModificationNode firstNode = networkModificationTreeService.createNode(studyEntity, rootNode.getIdNode(), createModificationNodeInfo(NODE_1_NAME), InsertMode.AFTER, null); - // the study points to a shared modification the user is not allowed to write on + // the study points to a shared modification the user is not allowed to write on, and not associated with the new tag doThrow(HttpClientErrorException.create(HttpStatus.FORBIDDEN, "Forbidden", null, null, null)) - .when(networkModificationService).assertReferencedModificationsAreWritable(List.of(firstNode.getModificationGroupUuid()), USER_ID); + .when(networkModificationService).assertRootNetworkTagsAllowed(List.of(firstNode.getModificationGroupUuid()), List.of(ROOT_NETWORK_TAG_2), USER_ID); UUID studyUuid = studyEntity.getId(); RootNetworkInfos renamingTagInfos = RootNetworkInfos.builder().id(rootNetworkUuid).tag(ROOT_NETWORK_TAG_2).build(); @@ -306,33 +307,35 @@ void testRenamingARootNetworkTagNeedsWritePermissionOnTheSharedModifications() t // neither the tag nor the applicabilities are touched assertEquals(ROOT_NETWORK_TAG_1, rootNetworkService.getRootNetworkTag(rootNetworkUuid)); - verify(networkModificationService, never()).renameRootNetworkTag(any(), any(), any()); + verify(networkModificationService, never()).renameRootNetworkTag(any(), any(), any(), any()); // an update keeping the tag is no rename, so it needs no permission updateRootNetwork(studyUuid, rootNetworkUuid, ROOT_NETWORK_TAG_1); - verify(networkModificationService, times(1)).assertReferencedModificationsAreWritable(any(), any()); + verify(networkModificationService, times(1)).assertRootNetworkTagsAllowed(any(), any(), any()); } @Test - void testHasSharedModifications() throws Exception { + void testGetSharedModificationsWriteAccess() throws Exception { StudyEntity studyEntity = TestUtils.createDummyStudy(NETWORK_UUID, CASE_UUID, CASE_NAME, CASE_FORMAT, REPORT_UUID); studyRepository.save(studyEntity); UUID studyUuid = studyEntity.getId(); // a study with no modification node contains no shared modification, and the server is not even asked - assertFalse(hasSharedModifications(studyUuid)); - verify(networkModificationService, never()).hasModificationReferences(any()); + assertEquals(SharedModificationsWriteAccess.NO_SHARED_MODIFICATION, getSharedModificationsWriteAccess(studyUuid)); + verify(networkModificationService, never()).getReferencedModificationsWriteAccess(any(), any()); NodeEntity rootNode = networkModificationTreeService.createRoot(studyEntity); NetworkModificationNode firstNode = networkModificationTreeService.createNode(studyEntity, rootNode.getIdNode(), createModificationNodeInfo(NODE_1_NAME), InsertMode.AFTER, null); - doReturn(true).when(networkModificationService).hasModificationReferences(List.of(firstNode.getModificationGroupUuid())); + doReturn(SharedModificationsWriteAccess.SOME_NOT_WRITABLE).when(networkModificationService) + .getReferencedModificationsWriteAccess(List.of(firstNode.getModificationGroupUuid()), USER_ID); - assertTrue(hasSharedModifications(studyUuid)); + assertEquals(SharedModificationsWriteAccess.SOME_NOT_WRITABLE, getSharedModificationsWriteAccess(studyUuid)); } - private boolean hasSharedModifications(UUID studyUuid) throws Exception { - return Boolean.parseBoolean(mockMvc.perform(get("/v1/studies/{studyUuid}/network-modifications/references/exists", studyUuid)) - .andExpect(status().isOk()).andReturn().getResponse().getContentAsString()); + private SharedModificationsWriteAccess getSharedModificationsWriteAccess(UUID studyUuid) throws Exception { + return objectMapper.readValue(mockMvc.perform(get("/v1/studies/{studyUuid}/network-modifications/references/write-access", studyUuid) + .header(HEADER_USER_ID, USER_ID)) + .andExpect(status().isOk()).andReturn().getResponse().getContentAsString(), SharedModificationsWriteAccess.class); } @Test diff --git a/src/test/java/org/gridsuite/study/server/service/NetworkModificationServiceTest.java b/src/test/java/org/gridsuite/study/server/service/NetworkModificationServiceTest.java index 0d94a827d..1b05e3bbd 100644 --- a/src/test/java/org/gridsuite/study/server/service/NetworkModificationServiceTest.java +++ b/src/test/java/org/gridsuite/study/server/service/NetworkModificationServiceTest.java @@ -9,6 +9,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import org.gridsuite.study.server.RemoteServicesProperties; import org.gridsuite.study.server.dto.ModificationReference; +import org.gridsuite.study.server.dto.SharedModificationsWriteAccess; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -213,12 +214,12 @@ void testRenameRootNetworkTag() { String expectedUrl = NETWORK_MODIFICATION_SERVER_URI + "/v1/network-modifications/root-network-tag" + "?groupUuids=" + firstGroupUuid + "&groupUuids=" + secondGroupUuid + "&oldTag=PH1&newTag=PH2"; - networkModificationService.renameRootNetworkTag(List.of(firstGroupUuid, secondGroupUuid), "PH1", "PH2"); + networkModificationService.renameRootNetworkTag(List.of(firstGroupUuid, secondGroupUuid), "PH1", "PH2", USER_ID); verify(restTemplate).exchange(eq(expectedUrl), eq(HttpMethod.PUT), org.mockito.ArgumentMatchers.>any(), eq(Void.class)); // there is nothing to rename without a group - networkModificationService.renameRootNetworkTag(List.of(), "PH1", "PH2"); + networkModificationService.renameRootNetworkTag(List.of(), "PH1", "PH2", USER_ID); verifyNoMoreInteractions(restTemplate); } @@ -256,19 +257,18 @@ void testDuplicateModificationsGroupAssociatesTheRootNetworkTags() { } @Test - void testAssertReferencedModificationsAreWritable() { + void testAssertRootNetworkTagsAllowed() { UUID groupUuid = UUID.randomUUID(); - String expectedUrl = NETWORK_MODIFICATION_SERVER_URI + "/v1/containers/references/authorized?uuids=" + groupUuid; - - networkModificationService.assertReferencedModificationsAreWritable(List.of(groupUuid), USER_ID); - - // the server needs the user to tell whether the shared modifications are writable HttpHeaders headers = new HttpHeaders(); headers.set(HEADER_USER_ID, USER_ID); - verify(restTemplate).exchange(expectedUrl, HttpMethod.GET, new HttpEntity<>(headers), Void.class); - // there is nothing to check without a container - networkModificationService.assertReferencedModificationsAreWritable(List.of(), USER_ID); + networkModificationService.assertRootNetworkTagsAllowed(List.of(groupUuid), List.of("PH1", "PH2"), USER_ID); + verify(restTemplate).exchange(NETWORK_MODIFICATION_SERVER_URI + "/v1/containers/root-network-tags/authorized?uuids=" + groupUuid + + "&tags=PH1&tags=PH2", HttpMethod.GET, new HttpEntity<>(headers), Void.class); + + // there is nothing to check without a tag, or without a container to check it on + networkModificationService.assertRootNetworkTagsAllowed(List.of(groupUuid), List.of(), USER_ID); + networkModificationService.assertRootNetworkTagsAllowed(List.of(), List.of("PH1"), USER_ID); verifyNoMoreInteractions(restTemplate); } @@ -282,6 +282,19 @@ void testHasModificationReferences() { verify(restTemplate).getForObject(eq(expectedUrl), eq(Boolean.class)); } + @Test + void testGetReferencedModificationsWriteAccess() { + UUID groupUuid = UUID.randomUUID(); + String expectedUrl = NETWORK_MODIFICATION_SERVER_URI + "/v1/containers/references/write-access?uuids=" + groupUuid; + HttpHeaders headers = new HttpHeaders(); + headers.set(HEADER_USER_ID, USER_ID); + when(restTemplate.exchange(expectedUrl, HttpMethod.GET, new HttpEntity<>(headers), SharedModificationsWriteAccess.class)) + .thenReturn(ResponseEntity.ok(SharedModificationsWriteAccess.SOME_NOT_WRITABLE)); + + assertThat(networkModificationService.getReferencedModificationsWriteAccess(List.of(groupUuid), USER_ID)) + .isEqualTo(SharedModificationsWriteAccess.SOME_NOT_WRITABLE); + } + private void expectUserIdIsForwarded(String expectedUrl) { HttpHeaders headers = new HttpHeaders(); headers.set(HEADER_USER_ID, USER_ID); diff --git a/src/test/java/org/gridsuite/study/server/studycontroller/StudyTestBase.java b/src/test/java/org/gridsuite/study/server/studycontroller/StudyTestBase.java index ee089e5d6..55672f8ba 100644 --- a/src/test/java/org/gridsuite/study/server/studycontroller/StudyTestBase.java +++ b/src/test/java/org/gridsuite/study/server/studycontroller/StudyTestBase.java @@ -395,6 +395,7 @@ void setup() { singleLineDiagramService.setSingleLineDiagramServerBaseUri(baseUrl); directoryService.setDirectoryServerServerBaseUri(baseUrl); networkModificationService.setNetworkModificationServerBaseUri(baseUrl); + wireMockStubs.stubRootNetworkTagsAllowed(); } protected UUID getRootNodeUuid(UUID studyUuid) { diff --git a/src/test/java/org/gridsuite/study/server/utils/TestUtils.java b/src/test/java/org/gridsuite/study/server/utils/TestUtils.java index 6e55e267e..83ff20e29 100644 --- a/src/test/java/org/gridsuite/study/server/utils/TestUtils.java +++ b/src/test/java/org/gridsuite/study/server/utils/TestUtils.java @@ -28,6 +28,7 @@ import org.gridsuite.study.server.service.StudyServerExecutionService; import org.gridsuite.study.server.utils.assertions.Assertions; import org.gridsuite.study.server.utils.wiremock.UserAdminServerStubs; +import org.gridsuite.study.server.utils.wiremock.WireMockStubs; import org.junit.platform.commons.util.StringUtils; import org.mockito.stubbing.Answer; import org.springframework.cloud.stream.binder.test.OutputDestination; @@ -384,6 +385,8 @@ public static void assertWiremockServerRequestsEmptyThenShutdown(WireMockServer // Remove quota operation consume/release requests that are auto-stubbed and do not need explicit verification wireMockServer.removeServeEventsMatching(WireMock.postRequestedFor(WireMock.urlPathMatching(UserAdminServerStubs.QUOTA_CONSUME_URL_PATTERN)).build()); wireMockServer.removeServeEventsMatching(WireMock.postRequestedFor(WireMock.urlPathMatching(UserAdminServerStubs.QUOTA_RELEASE_URL_PATTERN)).build()); + // Remove the root network tags checks on the shared modifications, auto-stubbed as well + wireMockServer.removeServeEventsMatching(WireMock.getRequestedFor(WireMock.urlPathEqualTo(WireMockStubs.ROOT_NETWORK_TAGS_AUTHORIZED_URL)).build()); var requests = wireMockServer.findAll(WireMock.anyRequestedFor(WireMock.anyUrl())); assertEquals(0, requests.size(), "Unverified WireMock requests found:\n" + requests.stream() .map(r -> "URL: " + r.getUrl() + ", Method: " + r.getMethod() + ", Body: " + r.getBodyAsString() + ", Params: " + r.getQueryParams()) @@ -399,6 +402,8 @@ public static void assertWiremockServerRequestsEmptyThenClear(WireMockServer wir // Remove quota operation consume/release requests that are auto-stubbed and do not need explicit verification wireMockServer.removeServeEventsMatching(WireMock.postRequestedFor(WireMock.urlPathMatching(UserAdminServerStubs.QUOTA_CONSUME_URL_PATTERN)).build()); wireMockServer.removeServeEventsMatching(WireMock.postRequestedFor(WireMock.urlPathMatching(UserAdminServerStubs.QUOTA_RELEASE_URL_PATTERN)).build()); + // Remove the root network tags checks on the shared modifications, auto-stubbed as well + wireMockServer.removeServeEventsMatching(WireMock.getRequestedFor(WireMock.urlPathEqualTo(WireMockStubs.ROOT_NETWORK_TAGS_AUTHORIZED_URL)).build()); assertEquals(0, wireMockServer.findAll(WireMock.anyRequestedFor(WireMock.anyUrl())).size()); } finally { wireMockServer.resetAll(); diff --git a/src/test/java/org/gridsuite/study/server/utils/wiremock/WireMockStubs.java b/src/test/java/org/gridsuite/study/server/utils/wiremock/WireMockStubs.java index f878c5f8d..9e2d71e58 100644 --- a/src/test/java/org/gridsuite/study/server/utils/wiremock/WireMockStubs.java +++ b/src/test/java/org/gridsuite/study/server/utils/wiremock/WireMockStubs.java @@ -45,6 +45,7 @@ public class WireMockStubs { private static final String URI_NETWORK_MODIFICATION_GROUPS = "/v1/groups"; private static final String URI_NETWORK_MODIFICATION_CONTAINERS = "/v1/containers"; + public static final String ROOT_NETWORK_TAGS_AUTHORIZED_URL = URI_NETWORK_MODIFICATION_CONTAINERS + "/root-network-tags/authorized"; public static final String FIRST_VARIANT_ID = "first_variant_id"; @@ -171,6 +172,12 @@ public void verifyNetworkElementsIdsPost(UUID stubUuid, String networkUuid, Stri requestBody); } + public UUID stubRootNetworkTagsAllowed() { + return wireMock.stubFor(WireMock.get(WireMock.urlPathEqualTo(ROOT_NETWORK_TAGS_AUTHORIZED_URL)) + .willReturn(WireMock.ok()) + ).getId(); + } + public UUID stubNetworkModificationCountGet(String groupUuid, Integer expectedCount) { return wireMock.stubFor(WireMock.get(WireMock.urlPathMatching(URI_NETWORK_MODIFICATION_CONTAINERS + DELIMITER + groupUuid + "/network-modifications-count")) .withQueryParam(QUERY_PARAM_STASHED, WireMock.equalTo("false")) From 705b2f27099d3f549df60e16776b0d1fa01bcb69 Mon Sep 17 00:00:00 2001 From: Florent MILLOT <75525996+flomillot@users.noreply.github.com> Date: Thu, 15 Oct 2026 17:42:06 +0200 Subject: [PATCH 04/12] Refuse pasting or importing shared modifications missing the study root network tags Signed-off-by: Florent MILLOT <75525996+flomillot@users.noreply.github.com> --- .../service/NetworkModificationService.java | 14 +++-- .../NetworkModificationTreeService.java | 12 +++++ .../study/server/service/StudyService.java | 51 ++++++++++++++++++- .../study/server/NetworkModificationTest.java | 32 ++++++++++++ .../RootNetworkApplicabilityTest.java | 4 +- .../NetworkModificationServiceTest.java | 19 ++++--- 6 files changed, 117 insertions(+), 15 deletions(-) diff --git a/src/main/java/org/gridsuite/study/server/service/NetworkModificationService.java b/src/main/java/org/gridsuite/study/server/service/NetworkModificationService.java index 52f205bb8..280ff75de 100644 --- a/src/main/java/org/gridsuite/study/server/service/NetworkModificationService.java +++ b/src/main/java/org/gridsuite/study/server/service/NetworkModificationService.java @@ -754,15 +754,19 @@ public SharedModificationsWriteAccess getReferencedModificationsWriteAccess(List } /** - * Asserts that the shared modifications the user cannot write on, among the ones the given containers point to, are - * all associated with the given root network tags. Throws a forbidden error when they are not. + * Asserts that the shared modifications the user cannot write on, among the given ones and the ones the content of + * the given containers, the given modifications and the given shared modifications refer to, are all associated with + * the given root network tags. Throws a forbidden error when they are not. */ - public void assertRootNetworkTagsAllowed(List containerUuids, List rootNetworkTags, String userId) { - if (rootNetworkTags.isEmpty() || containerUuids.isEmpty()) { + public void assertRootNetworkTagsAllowed(List containerUuids, List modificationUuids, List sharedUuids, + List rootNetworkTags, String userId) { + if (rootNetworkTags.isEmpty() || containerUuids.isEmpty() && modificationUuids.isEmpty() && sharedUuids.isEmpty()) { return; } String path = UriComponentsBuilder.fromPath(CONTAINERS + DELIMITER + "root-network-tags/authorized") - .queryParam(UUIDS, containerUuids) + .queryParam("containerUuids", containerUuids) + .queryParam("modificationUuids", modificationUuids) + .queryParam("sharedUuids", sharedUuids) .queryParam("tags", rootNetworkTags) .build().toUriString(); diff --git a/src/main/java/org/gridsuite/study/server/service/NetworkModificationTreeService.java b/src/main/java/org/gridsuite/study/server/service/NetworkModificationTreeService.java index a828bca22..93bbbc1dc 100644 --- a/src/main/java/org/gridsuite/study/server/service/NetworkModificationTreeService.java +++ b/src/main/java/org/gridsuite/study/server/service/NetworkModificationTreeService.java @@ -39,6 +39,7 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.function.Function; import java.util.stream.Collectors; +import java.util.stream.Stream; import static org.gridsuite.study.server.error.StudyBusinessErrorCode.*; @@ -821,6 +822,17 @@ public UUID getModificationGroupUuid(UUID nodeUuid) { return getNetworkModificationNodeInfoEntity(nodeUuid).getModificationGroupUuid(); } + /** + * @return the modification groups of the given node and of all its descendants + */ + @Transactional(readOnly = true) + public List getSubtreeModificationGroupUuids(UUID parentNodeUuid) { + List nodeUuids = Stream.concat(Stream.of(parentNodeUuid), getAllChildrenUuids(parentNodeUuid).stream()).toList(); + return networkModificationNodeInfoRepository.findAllByIdIn(nodeUuids).stream() + .map(NetworkModificationNodeInfoEntity::getModificationGroupUuid) + .toList(); + } + @Transactional(readOnly = true) public String getNetworkModifications(@NonNull UUID nodeUuid, boolean onlyStashed, boolean onlyMetadata, String userId) { return networkModificationService.getModifications(self.getModificationGroupUuid(nodeUuid), onlyStashed, onlyMetadata, userId); diff --git a/src/main/java/org/gridsuite/study/server/service/StudyService.java b/src/main/java/org/gridsuite/study/server/service/StudyService.java index b710521fc..f02c0d652 100644 --- a/src/main/java/org/gridsuite/study/server/service/StudyService.java +++ b/src/main/java/org/gridsuite/study/server/service/StudyService.java @@ -417,7 +417,25 @@ private void renameRootNetworkTagInApplicabilities(UUID studyUuid, String previo * write on, which they cannot associate with a new tag, must already be. */ private void assertCanAddRootNetworkTag(UUID studyUuid, String newTag, String userId) { - networkModificationService.assertRootNetworkTagsAllowed(getStudyModificationGroupUuids(studyUuid), List.of(newTag), userId); + networkModificationService.assertRootNetworkTagsAllowed(getStudyModificationGroupUuids(studyUuid), List.of(), List.of(), List.of(newTag), userId); + } + + /** + * Modifications written into a study must be associated with all its root network tags, including the shared + * modifications they point to: the ones the user cannot write on, which they cannot associate with a new tag, must + * already be. + * + * @param containerUuids groups or composites whose content is written into the study: only the shared modifications + * their content refers to are checked, not the containers themselves + * @param modificationUuids modifications written into the study: the shared modifications they and their content + * refer to are checked + * @param sharedUuids shared modifications the study is given a reference to: they are checked themselves, along + * with the shared modifications their content refers to + */ + private void assertCanWriteModificationsInStudy(UUID studyUuid, List containerUuids, List modificationUuids, + List sharedUuids, String userId) { + networkModificationService.assertRootNetworkTagsAllowed(containerUuids, modificationUuids, sharedUuids, + rootNetworkService.getStudyRootNetworkTags(studyUuid), userId); } public SharedModificationsWriteAccess getSharedModificationsWriteAccess(UUID studyUuid, String userId) { @@ -1436,6 +1454,8 @@ private void assertMoveStudyNode(UUID studyUuid, UUID nodeToMoveUuid, UUID refer @Transactional public void duplicateStudyNode(UUID sourceStudyUuid, UUID targetStudyUuid, UUID nodeToCopyUuid, UUID referenceNodeUuid, InsertMode insertMode, String userId) { assertDuplicateStudyNode(sourceStudyUuid, targetStudyUuid, nodeToCopyUuid, referenceNodeUuid, insertMode); + assertCanWriteModificationsInStudy(targetStudyUuid, List.of(networkModificationTreeService.getModificationGroupUuid(nodeToCopyUuid)), + List.of(), List.of(), userId); UUID duplicatedNodeUuid = networkModificationTreeService.duplicateStudyNode(nodeToCopyUuid, referenceNodeUuid, insertMode, userId); boolean invalidateBuild = networkModificationTreeService.hasModifications(nodeToCopyUuid, false); @@ -1448,6 +1468,8 @@ public void duplicateStudyNode(UUID sourceStudyUuid, UUID targetStudyUuid, UUID @Transactional public void moveStudyNode(UUID studyUuid, UUID nodeToMoveUuid, UUID referenceNodeUuid, InsertMode insertMode, String userId) { assertMoveStudyNode(studyUuid, nodeToMoveUuid, referenceNodeUuid, insertMode); + assertCanWriteModificationsInStudy(studyUuid, List.of(networkModificationTreeService.getModificationGroupUuid(nodeToMoveUuid)), + List.of(), List.of(), userId); List oldChildren = null; boolean shouldUnbuildChildren = networkModificationTreeService.hasModifications(nodeToMoveUuid, false); @@ -1478,6 +1500,8 @@ private void assertDuplicateStudySubtree(UUID sourceStudyUuid, UUID targetStudyU @Transactional public void duplicateStudySubtree(UUID sourceStudyUuid, UUID targetStudyUuid, UUID parentNodeToCopyUuid, UUID referenceNodeUuid, String userId) { assertDuplicateStudySubtree(sourceStudyUuid, targetStudyUuid, parentNodeToCopyUuid, referenceNodeUuid); + assertCanWriteModificationsInStudy(targetStudyUuid, networkModificationTreeService.getSubtreeModificationGroupUuids(parentNodeToCopyUuid), + List.of(), List.of(), userId); AbstractNode studySubTree = networkModificationTreeService.getStudySubtree(sourceStudyUuid, parentNodeToCopyUuid, null); StudyEntity studyEntity = getStudy(targetStudyUuid); UUID duplicatedNodeUuid = networkModificationTreeService.cloneStudyTree(studySubTree, referenceNodeUuid, studyEntity, userId); @@ -1494,6 +1518,8 @@ private void assertMoveStudySubtree(UUID studyUuid, UUID parentNodeToMoveUuid, U @Transactional public void moveStudySubtree(UUID studyUuid, UUID parentNodeToMoveUuid, UUID referenceNodeUuid, String userId) { assertMoveStudySubtree(studyUuid, parentNodeToMoveUuid, referenceNodeUuid); + assertCanWriteModificationsInStudy(studyUuid, networkModificationTreeService.getSubtreeModificationGroupUuids(parentNodeToMoveUuid), + List.of(), List.of(), userId); List allChildren = networkModificationTreeService.getChildrenUuids(parentNodeToMoveUuid); if (allChildren.contains(referenceNodeUuid)) { @@ -1856,6 +1882,11 @@ public void moveNetworkModifications( boolean isTargetInDifferentNodeTree, String userId) { boolean isSameNode = originNodeUuid.equals(targetNodeUuid); + // a reordering, within a node, write no new modification into it + if (!isSameNode) { + assertCanWriteModificationsInStudy(studyUuid, List.of(), modificationInfos.stream().map(ModificationMoveInfos::modificationUuid).toList(), + List.of(), userId); + } List targetChildrenUuids = networkModificationTreeService.getChildrenUuids(targetNodeUuid); List originChildrenUuids = isSameNode ? List.of() : networkModificationTreeService.getChildrenUuids(originNodeUuid); @@ -1942,6 +1973,7 @@ public void duplicateNetworkModifications( UUID targetNodeUuid, List modificationsUuids, String userId) { + assertCanWriteModificationsInStudy(targetStudyUuid, List.of(), modificationsUuids, List.of(), userId); duplicateModificationsOrInsertComposites(targetStudyUuid, targetNodeUuid, (groupUuid, modificationApplicationContexts) -> { NetworkModificationsResult result = networkModificationService.duplicateModifications(groupUuid, Pair.of(modificationsUuids, modificationApplicationContexts), userId); @@ -2011,12 +2043,27 @@ public void insertCompositeNetworkModifications( List compositesInfos, String userId, StudyConstants.CompositeModificationsActionType action) { + // CHECK ON SHARED MODIFICATIONS BEFORE INSERTION + // Only an INSERT of a composite marked as shared puts a reference to it in the study: the study then uses the + // shared composite itself, which is checked, along with its content. Any other composite (COPY, SPLIT, or INSERT + // not marked as shared) is copied: the study only gets its content, whose references to shared modifications are + // checked, the composite being passed as the container of that content. + boolean isInsert = action == StudyConstants.CompositeModificationsActionType.INSERT; + List copiedCompositeUuids = compositesInfos.stream() + .filter(composite -> !isInsert || !composite.isShared()) + .map(CompositeInfos::id) + .toList(); + List sharedCompositeUuids = compositesInfos.stream() + .filter(composite -> isInsert && composite.isShared()) + .map(CompositeInfos::id) + .toList(); + assertCanWriteModificationsInStudy(targetStudyUuid, copiedCompositeUuids, List.of(), sharedCompositeUuids, userId); duplicateModificationsOrInsertComposites( targetStudyUuid, targetNodeUuid, (groupUuid, modificationApplicationContexts) -> { NetworkModificationsResult result = networkModificationService.insertCompositeModifications(groupUuid, action, Pair.of(compositesInfos, modificationApplicationContexts)); - if (action == StudyConstants.CompositeModificationsActionType.INSERT) { + if (isInsert) { createCompositesReferences(compositesInfos, result.modificationUuids(), targetStudyUuid, targetNodeUuid, userId); } return result; diff --git a/src/test/java/org/gridsuite/study/server/NetworkModificationTest.java b/src/test/java/org/gridsuite/study/server/NetworkModificationTest.java index 8993b84bf..67cde945c 100644 --- a/src/test/java/org/gridsuite/study/server/NetworkModificationTest.java +++ b/src/test/java/org/gridsuite/study/server/NetworkModificationTest.java @@ -111,6 +111,7 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; /** @@ -336,6 +337,7 @@ void setup() { networkModificationService.setNetworkModificationServerBaseUri(baseUrl); directoryService.setDirectoryServerServerBaseUri(baseUrl); userAdminService.setUserAdminServerBaseUri(baseUrl); + wireMockStubs.stubRootNetworkTagsAllowed(); buildOkStubId = wireMockServer.stubFor(WireMock.post(WireMock.urlPathEqualTo("/v1/networks/" + NETWORK_UUID_STRING + "/build")) .withPostServeAction(POST_ACTION_SEND_INPUT, Map.of("payload", DEFAULT_BUILD_RESULT, "destination", "build.result")) @@ -2355,6 +2357,36 @@ void testAssembleModificationsIntoNewComposite() throws Exception { ); } + @Test + void testPasteForbiddenByASharedModificationNotAssociatedWithTheRootNetworkTags() throws Exception { + StudyEntity studyEntity = insertDummyStudy(UUID.fromString(NETWORK_UUID_STRING), CASE_UUID, "UCTE"); + UUID studyUuid = studyEntity.getId(); + UUID nodeUuid = createNetworkModificationNode(studyUuid, getRootNode(studyUuid).getId(), UUID.randomUUID(), VARIANT_ID, "New node 1", "userId").getId(); + UUID modificationUuid = UUID.randomUUID(); + String forbiddenBody = mapper.writeValueAsString(PowsyblWsProblemDetail.builder(HttpStatus.FORBIDDEN) + .server("network-modification-server") + .detail("forbidden") + .businessErrorCode("modification.rootNetworkTagsForbiddenByShared") + .businessErrorValues(Map.of("allowedTags", "A, B", "allowedTagsCount", 2)) + .build()); + UUID forbiddenStubId = wireMockServer.stubFor(WireMock.get(WireMock.urlPathEqualTo(WireMockStubs.ROOT_NETWORK_TAGS_AUTHORIZED_URL)) + .atPriority(1) + .willReturn(WireMock.forbidden().withHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_PROBLEM_JSON_VALUE).withBody(forbiddenBody))) + .getId(); + + // the error tells the user the tags they may use, and nothing is copied + mockMvc.perform(put("/v1/studies/{studyUuid}/nodes/{nodeUuid}", studyUuid, nodeUuid) + .contentType(MediaType.APPLICATION_JSON) + .content(mapper.writeValueAsString(List.of(new ModificationCopyInfos(modificationUuid, null)))) + .header(USER_ID_HEADER, "userId")) + .andExpect(status().isForbidden()) + .andExpect(jsonPath("$.businessErrorCode").value("modification.rootNetworkTagsForbiddenByShared")) + .andExpect(jsonPath("$.businessErrorValues.allowedTags").value("A, B")); + + WireMockUtils.verifyGetRequest(wireMockServer, forbiddenStubId, WireMockStubs.ROOT_NETWORK_TAGS_AUTHORIZED_URL, + Map.of("modificationUuids", WireMock.equalTo(modificationUuid.toString()), "tags", WireMock.equalTo(studyEntity.getFirstRootNetwork().getTag()))); + } + @Test void testDuplicateModification() throws Exception { String userId = "userId"; diff --git a/src/test/java/org/gridsuite/study/server/rootnetworks/RootNetworkApplicabilityTest.java b/src/test/java/org/gridsuite/study/server/rootnetworks/RootNetworkApplicabilityTest.java index 8d6663bab..8c571978b 100644 --- a/src/test/java/org/gridsuite/study/server/rootnetworks/RootNetworkApplicabilityTest.java +++ b/src/test/java/org/gridsuite/study/server/rootnetworks/RootNetworkApplicabilityTest.java @@ -299,7 +299,7 @@ void testRenamingARootNetworkTagNeedsTheSharedModificationsTheUserCannotWriteOnT // the study points to a shared modification the user is not allowed to write on, and not associated with the new tag doThrow(HttpClientErrorException.create(HttpStatus.FORBIDDEN, "Forbidden", null, null, null)) - .when(networkModificationService).assertRootNetworkTagsAllowed(List.of(firstNode.getModificationGroupUuid()), List.of(ROOT_NETWORK_TAG_2), USER_ID); + .when(networkModificationService).assertRootNetworkTagsAllowed(List.of(firstNode.getModificationGroupUuid()), List.of(), List.of(), List.of(ROOT_NETWORK_TAG_2), USER_ID); UUID studyUuid = studyEntity.getId(); RootNetworkInfos renamingTagInfos = RootNetworkInfos.builder().id(rootNetworkUuid).tag(ROOT_NETWORK_TAG_2).build(); @@ -311,7 +311,7 @@ void testRenamingARootNetworkTagNeedsTheSharedModificationsTheUserCannotWriteOnT // an update keeping the tag is no rename, so it needs no permission updateRootNetwork(studyUuid, rootNetworkUuid, ROOT_NETWORK_TAG_1); - verify(networkModificationService, times(1)).assertRootNetworkTagsAllowed(any(), any(), any()); + verify(networkModificationService, times(1)).assertRootNetworkTagsAllowed(any(), any(), any(), any(), any()); } @Test diff --git a/src/test/java/org/gridsuite/study/server/service/NetworkModificationServiceTest.java b/src/test/java/org/gridsuite/study/server/service/NetworkModificationServiceTest.java index 1b05e3bbd..4b640fe33 100644 --- a/src/test/java/org/gridsuite/study/server/service/NetworkModificationServiceTest.java +++ b/src/test/java/org/gridsuite/study/server/service/NetworkModificationServiceTest.java @@ -259,16 +259,23 @@ void testDuplicateModificationsGroupAssociatesTheRootNetworkTags() { @Test void testAssertRootNetworkTagsAllowed() { UUID groupUuid = UUID.randomUUID(); + UUID modificationUuid = UUID.randomUUID(); + UUID sharedUuid = UUID.randomUUID(); HttpHeaders headers = new HttpHeaders(); headers.set(HEADER_USER_ID, USER_ID); - networkModificationService.assertRootNetworkTagsAllowed(List.of(groupUuid), List.of("PH1", "PH2"), USER_ID); - verify(restTemplate).exchange(NETWORK_MODIFICATION_SERVER_URI + "/v1/containers/root-network-tags/authorized?uuids=" + groupUuid - + "&tags=PH1&tags=PH2", HttpMethod.GET, new HttpEntity<>(headers), Void.class); + networkModificationService.assertRootNetworkTagsAllowed(List.of(groupUuid), List.of(), List.of(), List.of("PH1", "PH2"), USER_ID); + verify(restTemplate).exchange(NETWORK_MODIFICATION_SERVER_URI + "/v1/containers/root-network-tags/authorized?containerUuids=" + groupUuid + + "&modificationUuids&sharedUuids&tags=PH1&tags=PH2", HttpMethod.GET, new HttpEntity<>(headers), Void.class); + + // the modifications and the shared modifications are checked too, an empty list is sent by its name only + networkModificationService.assertRootNetworkTagsAllowed(List.of(), List.of(modificationUuid), List.of(sharedUuid), List.of("PH1"), USER_ID); + verify(restTemplate).exchange(NETWORK_MODIFICATION_SERVER_URI + "/v1/containers/root-network-tags/authorized?containerUuids&modificationUuids=" + modificationUuid + + "&sharedUuids=" + sharedUuid + "&tags=PH1", HttpMethod.GET, new HttpEntity<>(headers), Void.class); - // there is nothing to check without a tag, or without a container to check it on - networkModificationService.assertRootNetworkTagsAllowed(List.of(groupUuid), List.of(), USER_ID); - networkModificationService.assertRootNetworkTagsAllowed(List.of(), List.of("PH1"), USER_ID); + // there is nothing to check without a tag, or without anything to check it on + networkModificationService.assertRootNetworkTagsAllowed(List.of(groupUuid), List.of(), List.of(), List.of(), USER_ID); + networkModificationService.assertRootNetworkTagsAllowed(List.of(), List.of(), List.of(), List.of("PH1"), USER_ID); verifyNoMoreInteractions(restTemplate); } From e54e8b5ddd80acb8e650cbb47995ef9a032ef1e2 Mon Sep 17 00:00:00 2001 From: Florent MILLOT <75525996+flomillot@users.noreply.github.com> Date: Fri, 16 Oct 2026 12:24:11 +0200 Subject: [PATCH 05/12] Check root network tags only on pastes from another study Signed-off-by: Florent MILLOT <75525996+flomillot@users.noreply.github.com> --- .../study/server/service/StudyService.java | 21 +++++++------------ .../studycontroller/NodeControllerTest.java | 9 ++++++++ 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/src/main/java/org/gridsuite/study/server/service/StudyService.java b/src/main/java/org/gridsuite/study/server/service/StudyService.java index f02c0d652..189b83640 100644 --- a/src/main/java/org/gridsuite/study/server/service/StudyService.java +++ b/src/main/java/org/gridsuite/study/server/service/StudyService.java @@ -1454,8 +1454,10 @@ private void assertMoveStudyNode(UUID studyUuid, UUID nodeToMoveUuid, UUID refer @Transactional public void duplicateStudyNode(UUID sourceStudyUuid, UUID targetStudyUuid, UUID nodeToCopyUuid, UUID referenceNodeUuid, InsertMode insertMode, String userId) { assertDuplicateStudyNode(sourceStudyUuid, targetStudyUuid, nodeToCopyUuid, referenceNodeUuid, insertMode); - assertCanWriteModificationsInStudy(targetStudyUuid, List.of(networkModificationTreeService.getModificationGroupUuid(nodeToCopyUuid)), - List.of(), List.of(), userId); + if (!sourceStudyUuid.equals(targetStudyUuid)) { + assertCanWriteModificationsInStudy(targetStudyUuid, List.of(networkModificationTreeService.getModificationGroupUuid(nodeToCopyUuid)), + List.of(), List.of(), userId); + } UUID duplicatedNodeUuid = networkModificationTreeService.duplicateStudyNode(nodeToCopyUuid, referenceNodeUuid, insertMode, userId); boolean invalidateBuild = networkModificationTreeService.hasModifications(nodeToCopyUuid, false); @@ -1468,8 +1470,6 @@ public void duplicateStudyNode(UUID sourceStudyUuid, UUID targetStudyUuid, UUID @Transactional public void moveStudyNode(UUID studyUuid, UUID nodeToMoveUuid, UUID referenceNodeUuid, InsertMode insertMode, String userId) { assertMoveStudyNode(studyUuid, nodeToMoveUuid, referenceNodeUuid, insertMode); - assertCanWriteModificationsInStudy(studyUuid, List.of(networkModificationTreeService.getModificationGroupUuid(nodeToMoveUuid)), - List.of(), List.of(), userId); List oldChildren = null; boolean shouldUnbuildChildren = networkModificationTreeService.hasModifications(nodeToMoveUuid, false); @@ -1500,8 +1500,10 @@ private void assertDuplicateStudySubtree(UUID sourceStudyUuid, UUID targetStudyU @Transactional public void duplicateStudySubtree(UUID sourceStudyUuid, UUID targetStudyUuid, UUID parentNodeToCopyUuid, UUID referenceNodeUuid, String userId) { assertDuplicateStudySubtree(sourceStudyUuid, targetStudyUuid, parentNodeToCopyUuid, referenceNodeUuid); - assertCanWriteModificationsInStudy(targetStudyUuid, networkModificationTreeService.getSubtreeModificationGroupUuids(parentNodeToCopyUuid), - List.of(), List.of(), userId); + if (!sourceStudyUuid.equals(targetStudyUuid)) { + assertCanWriteModificationsInStudy(targetStudyUuid, networkModificationTreeService.getSubtreeModificationGroupUuids(parentNodeToCopyUuid), + List.of(), List.of(), userId); + } AbstractNode studySubTree = networkModificationTreeService.getStudySubtree(sourceStudyUuid, parentNodeToCopyUuid, null); StudyEntity studyEntity = getStudy(targetStudyUuid); UUID duplicatedNodeUuid = networkModificationTreeService.cloneStudyTree(studySubTree, referenceNodeUuid, studyEntity, userId); @@ -1518,8 +1520,6 @@ private void assertMoveStudySubtree(UUID studyUuid, UUID parentNodeToMoveUuid, U @Transactional public void moveStudySubtree(UUID studyUuid, UUID parentNodeToMoveUuid, UUID referenceNodeUuid, String userId) { assertMoveStudySubtree(studyUuid, parentNodeToMoveUuid, referenceNodeUuid); - assertCanWriteModificationsInStudy(studyUuid, networkModificationTreeService.getSubtreeModificationGroupUuids(parentNodeToMoveUuid), - List.of(), List.of(), userId); List allChildren = networkModificationTreeService.getChildrenUuids(parentNodeToMoveUuid); if (allChildren.contains(referenceNodeUuid)) { @@ -1882,11 +1882,6 @@ public void moveNetworkModifications( boolean isTargetInDifferentNodeTree, String userId) { boolean isSameNode = originNodeUuid.equals(targetNodeUuid); - // a reordering, within a node, write no new modification into it - if (!isSameNode) { - assertCanWriteModificationsInStudy(studyUuid, List.of(), modificationInfos.stream().map(ModificationMoveInfos::modificationUuid).toList(), - List.of(), userId); - } List targetChildrenUuids = networkModificationTreeService.getChildrenUuids(targetNodeUuid); List originChildrenUuids = isSameNode ? List.of() : networkModificationTreeService.getChildrenUuids(originNodeUuid); diff --git a/src/test/java/org/gridsuite/study/server/studycontroller/NodeControllerTest.java b/src/test/java/org/gridsuite/study/server/studycontroller/NodeControllerTest.java index 5a89f440e..596e286b1 100644 --- a/src/test/java/org/gridsuite/study/server/studycontroller/NodeControllerTest.java +++ b/src/test/java/org/gridsuite/study/server/studycontroller/NodeControllerTest.java @@ -20,6 +20,8 @@ import org.gridsuite.study.server.notification.NotificationService; import org.gridsuite.study.server.utils.MatcherReportLog; import org.gridsuite.study.server.utils.TestUtils; +import org.gridsuite.study.server.utils.wiremock.WireMockStubs; +import org.gridsuite.study.server.utils.wiremock.WireMockUtilsCriteria; import org.junit.jupiter.api.Test; import org.springframework.data.util.Pair; import org.springframework.http.HttpHeaders; @@ -652,6 +654,9 @@ void testDuplicateNode() throws Exception { assertEquals(BuildStatus.BUILT, networkModificationTreeService.getNodeBuildStatus(node3.getId(), rootNetworkUuid).getGlobalBuildStatus()); duplicateNode(study1Uuid, study1Uuid, emptyNode, node3.getId(), InsertMode.BEFORE, false, userId); assertEquals(BuildStatus.BUILT, networkModificationTreeService.getNodeBuildStatus(node3.getId(), rootNetworkUuid).getGlobalBuildStatus()); + + // copies within the study are not checked against its shared modifications + WireMockUtilsCriteria.verifyGetRequest(wireMockServer, WireMockStubs.ROOT_NETWORK_TAGS_AUTHORIZED_URL, Map.of(), 0); } @Test @@ -761,6 +766,8 @@ void testDuplicateSubtree() throws Exception { checkSubtreeCreatedMessageSent(study1Uuid, nodesAfterDuplication.get(0), node4.getId()); checkElementUpdatedMessageSent(study1Uuid, userId); wireMockStubs.verifyDuplicateModificationGroup(stubDuplicateUuid, 3); + // copies within the study are not checked against its shared modifications + WireMockUtilsCriteria.verifyGetRequest(wireMockServer, WireMockStubs.ROOT_NETWORK_TAGS_AUTHORIZED_URL, Map.of(), 0); Map allNodesInfos = networkModificationTreeService.getAllStudyNodesByUuid(study1Uuid); networkModificationTreeService.getAllChildrenUuids(node4.getId()).forEach(childUuid -> @@ -879,6 +886,8 @@ void testDuplicateNodeBetweenStudies() throws Exception { // duplicate the node1 from study 1 after node2 from study 2 UUID duplicatedNodeUuid = duplicateNode(study1Uuid, study2Uuid, node1, study2Node2.getId(), InsertMode.AFTER, true, userId); + WireMockUtilsCriteria.verifyGetRequest(wireMockServer, WireMockStubs.ROOT_NETWORK_TAGS_AUTHORIZED_URL, + Map.of("containerUuids", WireMock.equalTo(node1.getModificationGroupUuid().toString())), 1); //node2 should now have 1 child allNodes = networkModificationTreeService.getAllNodes(study2Uuid); From 32143dddd67f73b249b0985fb1edd7534a977ae0 Mon Sep 17 00:00:00 2001 From: Florent MILLOT <75525996+flomillot@users.noreply.github.com> Date: Fri, 16 Oct 2026 17:47:05 +0200 Subject: [PATCH 06/12] Tell whether the user can write on all, some or none of the shared modifications Signed-off-by: Florent MILLOT <75525996+flomillot@users.noreply.github.com> --- .../gridsuite/study/server/controller/StudyController.java | 2 +- .../study/server/dto/SharedModificationsWriteAccess.java | 3 ++- .../server/rootnetworks/RootNetworkApplicabilityTest.java | 4 ++-- .../study/server/service/NetworkModificationServiceTest.java | 4 ++-- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/gridsuite/study/server/controller/StudyController.java b/src/main/java/org/gridsuite/study/server/controller/StudyController.java index f424c0ade..ebefb5c88 100644 --- a/src/main/java/org/gridsuite/study/server/controller/StudyController.java +++ b/src/main/java/org/gridsuite/study/server/controller/StudyController.java @@ -950,7 +950,7 @@ public ResponseEntity> getAvailableSvgComponentLibraries() { } @GetMapping(value = "/studies/{studyUuid}/network-modifications/references/write-access") - @Operation(summary = "Whether the study contains shared modifications, including nested in its composites, and, if so, whether the user can write on all of them") + @Operation(summary = "Whether the study contains shared modifications, including nested in its composites, and, if so, whether the user can write on all, some or none of them") @ApiResponse(responseCode = "200", description = "The write access on the shared modifications was returned") public ResponseEntity getSharedModificationsWriteAccess(@PathVariable("studyUuid") UUID studyUuid, @RequestHeader(HEADER_USER_ID) String userId) { diff --git a/src/main/java/org/gridsuite/study/server/dto/SharedModificationsWriteAccess.java b/src/main/java/org/gridsuite/study/server/dto/SharedModificationsWriteAccess.java index d57a90cbd..2887b1e97 100644 --- a/src/main/java/org/gridsuite/study/server/dto/SharedModificationsWriteAccess.java +++ b/src/main/java/org/gridsuite/study/server/dto/SharedModificationsWriteAccess.java @@ -14,5 +14,6 @@ public enum SharedModificationsWriteAccess { NO_SHARED_MODIFICATION, ALL_WRITABLE, - SOME_NOT_WRITABLE + SOME_WRITABLE, + NONE_WRITABLE } diff --git a/src/test/java/org/gridsuite/study/server/rootnetworks/RootNetworkApplicabilityTest.java b/src/test/java/org/gridsuite/study/server/rootnetworks/RootNetworkApplicabilityTest.java index 8d6663bab..0367d4d12 100644 --- a/src/test/java/org/gridsuite/study/server/rootnetworks/RootNetworkApplicabilityTest.java +++ b/src/test/java/org/gridsuite/study/server/rootnetworks/RootNetworkApplicabilityTest.java @@ -326,10 +326,10 @@ void testGetSharedModificationsWriteAccess() throws Exception { NodeEntity rootNode = networkModificationTreeService.createRoot(studyEntity); NetworkModificationNode firstNode = networkModificationTreeService.createNode(studyEntity, rootNode.getIdNode(), createModificationNodeInfo(NODE_1_NAME), InsertMode.AFTER, null); - doReturn(SharedModificationsWriteAccess.SOME_NOT_WRITABLE).when(networkModificationService) + doReturn(SharedModificationsWriteAccess.NONE_WRITABLE).when(networkModificationService) .getReferencedModificationsWriteAccess(List.of(firstNode.getModificationGroupUuid()), USER_ID); - assertEquals(SharedModificationsWriteAccess.SOME_NOT_WRITABLE, getSharedModificationsWriteAccess(studyUuid)); + assertEquals(SharedModificationsWriteAccess.NONE_WRITABLE, getSharedModificationsWriteAccess(studyUuid)); } private SharedModificationsWriteAccess getSharedModificationsWriteAccess(UUID studyUuid) throws Exception { diff --git a/src/test/java/org/gridsuite/study/server/service/NetworkModificationServiceTest.java b/src/test/java/org/gridsuite/study/server/service/NetworkModificationServiceTest.java index 1b05e3bbd..52c6070a3 100644 --- a/src/test/java/org/gridsuite/study/server/service/NetworkModificationServiceTest.java +++ b/src/test/java/org/gridsuite/study/server/service/NetworkModificationServiceTest.java @@ -289,10 +289,10 @@ void testGetReferencedModificationsWriteAccess() { HttpHeaders headers = new HttpHeaders(); headers.set(HEADER_USER_ID, USER_ID); when(restTemplate.exchange(expectedUrl, HttpMethod.GET, new HttpEntity<>(headers), SharedModificationsWriteAccess.class)) - .thenReturn(ResponseEntity.ok(SharedModificationsWriteAccess.SOME_NOT_WRITABLE)); + .thenReturn(ResponseEntity.ok(SharedModificationsWriteAccess.NONE_WRITABLE)); assertThat(networkModificationService.getReferencedModificationsWriteAccess(List.of(groupUuid), USER_ID)) - .isEqualTo(SharedModificationsWriteAccess.SOME_NOT_WRITABLE); + .isEqualTo(SharedModificationsWriteAccess.NONE_WRITABLE); } private void expectUserIdIsForwarded(String expectedUrl) { From 4cea6c95eabef86b6cd20dd2d1bd264aadce8397 Mon Sep 17 00:00:00 2001 From: Florent MILLOT Date: Thu, 22 Oct 2026 11:50:23 +0200 Subject: [PATCH 07/12] Extract the check of the inserted composites Signed-off-by: Florent MILLOT --- .../study/server/service/StudyService.java | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/src/main/java/org/gridsuite/study/server/service/StudyService.java b/src/main/java/org/gridsuite/study/server/service/StudyService.java index 91e0852db..6625ee6a4 100644 --- a/src/main/java/org/gridsuite/study/server/service/StudyService.java +++ b/src/main/java/org/gridsuite/study/server/service/StudyService.java @@ -2001,21 +2001,8 @@ public void insertCompositeNetworkModifications( List compositesInfos, String userId, StudyConstants.CompositeModificationsActionType action) { - // CHECK ON SHARED MODIFICATIONS BEFORE INSERTION - // Only an INSERT of a composite marked as shared puts a reference to it in the study: the study then uses the - // shared composite itself, which is checked, along with its content. Any other composite (COPY, SPLIT, or INSERT - // not marked as shared) is copied: the study only gets its content, whose references to shared modifications are - // checked, the composite being passed as the container of that content. boolean isInsert = action == StudyConstants.CompositeModificationsActionType.INSERT; - List copiedCompositeUuids = compositesInfos.stream() - .filter(composite -> !isInsert || !composite.isShared()) - .map(CompositeInfos::id) - .toList(); - List sharedCompositeUuids = compositesInfos.stream() - .filter(composite -> isInsert && composite.isShared()) - .map(CompositeInfos::id) - .toList(); - assertCanWriteModificationsInStudy(targetStudyUuid, copiedCompositeUuids, List.of(), sharedCompositeUuids, userId); + assertCanWriteCompositesInStudy(targetStudyUuid, compositesInfos, isInsert, userId); duplicateModificationsOrInsertComposites( targetStudyUuid, targetNodeUuid, @@ -2030,6 +2017,21 @@ public void insertCompositeNetworkModifications( ); } + /** + * Only an INSERT of a composite marked as shared puts a reference to it in the study: the study then uses the shared + * composite itself, which is checked, along with its content. Any other composite (COPY, SPLIT, or INSERT not marked as + * shared) is copied: the study only gets its content, whose references to shared modifications are checked, the + * composite being passed as the container of that content. + */ + private void assertCanWriteCompositesInStudy(UUID studyUuid, List compositesInfos, boolean isInsert, String userId) { + Map> uuidsByShared = compositesInfos.stream() + .collect(Collectors.partitioningBy(composite -> isInsert && composite.isShared(), + Collectors.mapping(CompositeInfos::id, Collectors.toList()))); + List sharedCompositeUuids = uuidsByShared.get(true); + List copiedCompositeUuids = uuidsByShared.get(false); + assertCanWriteModificationsInStudy(studyUuid, copiedCompositeUuids, List.of(), sharedCompositeUuids, userId); + } + private void createCompositesReferences(List compositesInfos, List insertedModificationUuids, UUID studyUuid, UUID nodeUuid, String userId) { for (int i = 0; i < compositesInfos.size(); i++) { From 507dbbbd5351c586a874ba83feb615a0f64818f1 Mon Sep 17 00:00:00 2001 From: Florent MILLOT Date: Thu, 22 Oct 2026 12:13:53 +0200 Subject: [PATCH 08/12] Send the root network tags check in the request body Signed-off-by: Florent MILLOT --- .../server/dto/RootNetworkTagsCheckInfos.java | 16 ++++++++++++++++ .../NetworkModificationRestService.java | 9 +++------ .../NetworkModificationServiceTest.java | 6 ++++-- .../gridsuite/study/server/utils/TestUtils.java | 4 ++-- .../server/utils/wiremock/WireMockStubs.java | 2 +- 5 files changed, 26 insertions(+), 11 deletions(-) create mode 100644 src/main/java/org/gridsuite/study/server/dto/RootNetworkTagsCheckInfos.java diff --git a/src/main/java/org/gridsuite/study/server/dto/RootNetworkTagsCheckInfos.java b/src/main/java/org/gridsuite/study/server/dto/RootNetworkTagsCheckInfos.java new file mode 100644 index 000000000..3256a94d1 --- /dev/null +++ b/src/main/java/org/gridsuite/study/server/dto/RootNetworkTagsCheckInfos.java @@ -0,0 +1,16 @@ +/** + * Copyright (c) 2026, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.gridsuite.study.server.dto; + +import java.util.List; +import java.util.UUID; + +/** + * Modifications to check against root network tags. + */ +public record RootNetworkTagsCheckInfos(List containerUuids, List tags) { +} diff --git a/src/main/java/org/gridsuite/study/server/service/networkmodification/NetworkModificationRestService.java b/src/main/java/org/gridsuite/study/server/service/networkmodification/NetworkModificationRestService.java index be15c7ac4..cf3a1c799 100644 --- a/src/main/java/org/gridsuite/study/server/service/networkmodification/NetworkModificationRestService.java +++ b/src/main/java/org/gridsuite/study/server/service/networkmodification/NetworkModificationRestService.java @@ -14,6 +14,7 @@ import org.gridsuite.study.server.dto.BuildInfos; import org.gridsuite.study.server.dto.ModificationReference; import org.gridsuite.study.server.dto.NodeReceiver; +import org.gridsuite.study.server.dto.RootNetworkTagsCheckInfos; import org.gridsuite.study.server.dto.SharedModificationsWriteAccess; import org.gridsuite.study.server.dto.modification.*; import org.gridsuite.study.server.dto.workflow.AbstractWorkflowInfos; @@ -762,12 +763,8 @@ public void assertRootNetworkTagsAllowed(List containerUuids, List if (rootNetworkTags.isEmpty() || containerUuids.isEmpty()) { return; } - String path = UriComponentsBuilder.fromPath(CONTAINERS + DELIMITER + "root-network-tags/authorized") - .queryParam(UUIDS, containerUuids) - .queryParam("tags", rootNetworkTags) - .build().toUriString(); - - restTemplate.exchange(getNetworkModificationServerURI(false) + path, HttpMethod.GET, userIdEntity(userId), Void.class); + restTemplate.exchange(getNetworkModificationServerURI(false) + CONTAINERS + DELIMITER + "root-network-tags/authorized", + HttpMethod.POST, userIdEntity(userId, new RootNetworkTagsCheckInfos(containerUuids, rootNetworkTags)), Void.class); } /** diff --git a/src/test/java/org/gridsuite/study/server/service/networkmodification/NetworkModificationServiceTest.java b/src/test/java/org/gridsuite/study/server/service/networkmodification/NetworkModificationServiceTest.java index 785c7ab68..de2ab2e93 100644 --- a/src/test/java/org/gridsuite/study/server/service/networkmodification/NetworkModificationServiceTest.java +++ b/src/test/java/org/gridsuite/study/server/service/networkmodification/NetworkModificationServiceTest.java @@ -9,6 +9,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import org.gridsuite.study.server.RemoteServicesProperties; import org.gridsuite.study.server.dto.ModificationReference; +import org.gridsuite.study.server.dto.RootNetworkTagsCheckInfos; import org.gridsuite.study.server.dto.SharedModificationsWriteAccess; import org.gridsuite.study.server.service.RootNetworkService; import org.junit.jupiter.api.BeforeEach; @@ -265,10 +266,11 @@ void testAssertRootNetworkTagsAllowed() { UUID groupUuid = UUID.randomUUID(); HttpHeaders headers = new HttpHeaders(); headers.set(HEADER_USER_ID, USER_ID); + headers.setContentType(MediaType.APPLICATION_JSON); networkModificationService.assertRootNetworkTagsAllowed(List.of(groupUuid), List.of("PH1", "PH2"), USER_ID); - verify(restTemplate).exchange(NETWORK_MODIFICATION_SERVER_URI + "/v1/containers/root-network-tags/authorized?uuids=" + groupUuid - + "&tags=PH1&tags=PH2", HttpMethod.GET, new HttpEntity<>(headers), Void.class); + verify(restTemplate).exchange(NETWORK_MODIFICATION_SERVER_URI + "/v1/containers/root-network-tags/authorized", HttpMethod.POST, + new HttpEntity<>(new RootNetworkTagsCheckInfos(List.of(groupUuid), List.of("PH1", "PH2")), headers), Void.class); // there is nothing to check without a tag, or without a container to check it on networkModificationService.assertRootNetworkTagsAllowed(List.of(groupUuid), List.of(), USER_ID); diff --git a/src/test/java/org/gridsuite/study/server/utils/TestUtils.java b/src/test/java/org/gridsuite/study/server/utils/TestUtils.java index 83ff20e29..4b8be7e2d 100644 --- a/src/test/java/org/gridsuite/study/server/utils/TestUtils.java +++ b/src/test/java/org/gridsuite/study/server/utils/TestUtils.java @@ -386,7 +386,7 @@ public static void assertWiremockServerRequestsEmptyThenShutdown(WireMockServer wireMockServer.removeServeEventsMatching(WireMock.postRequestedFor(WireMock.urlPathMatching(UserAdminServerStubs.QUOTA_CONSUME_URL_PATTERN)).build()); wireMockServer.removeServeEventsMatching(WireMock.postRequestedFor(WireMock.urlPathMatching(UserAdminServerStubs.QUOTA_RELEASE_URL_PATTERN)).build()); // Remove the root network tags checks on the shared modifications, auto-stubbed as well - wireMockServer.removeServeEventsMatching(WireMock.getRequestedFor(WireMock.urlPathEqualTo(WireMockStubs.ROOT_NETWORK_TAGS_AUTHORIZED_URL)).build()); + wireMockServer.removeServeEventsMatching(WireMock.postRequestedFor(WireMock.urlPathEqualTo(WireMockStubs.ROOT_NETWORK_TAGS_AUTHORIZED_URL)).build()); var requests = wireMockServer.findAll(WireMock.anyRequestedFor(WireMock.anyUrl())); assertEquals(0, requests.size(), "Unverified WireMock requests found:\n" + requests.stream() .map(r -> "URL: " + r.getUrl() + ", Method: " + r.getMethod() + ", Body: " + r.getBodyAsString() + ", Params: " + r.getQueryParams()) @@ -403,7 +403,7 @@ public static void assertWiremockServerRequestsEmptyThenClear(WireMockServer wir wireMockServer.removeServeEventsMatching(WireMock.postRequestedFor(WireMock.urlPathMatching(UserAdminServerStubs.QUOTA_CONSUME_URL_PATTERN)).build()); wireMockServer.removeServeEventsMatching(WireMock.postRequestedFor(WireMock.urlPathMatching(UserAdminServerStubs.QUOTA_RELEASE_URL_PATTERN)).build()); // Remove the root network tags checks on the shared modifications, auto-stubbed as well - wireMockServer.removeServeEventsMatching(WireMock.getRequestedFor(WireMock.urlPathEqualTo(WireMockStubs.ROOT_NETWORK_TAGS_AUTHORIZED_URL)).build()); + wireMockServer.removeServeEventsMatching(WireMock.postRequestedFor(WireMock.urlPathEqualTo(WireMockStubs.ROOT_NETWORK_TAGS_AUTHORIZED_URL)).build()); assertEquals(0, wireMockServer.findAll(WireMock.anyRequestedFor(WireMock.anyUrl())).size()); } finally { wireMockServer.resetAll(); diff --git a/src/test/java/org/gridsuite/study/server/utils/wiremock/WireMockStubs.java b/src/test/java/org/gridsuite/study/server/utils/wiremock/WireMockStubs.java index 9e2d71e58..010c367af 100644 --- a/src/test/java/org/gridsuite/study/server/utils/wiremock/WireMockStubs.java +++ b/src/test/java/org/gridsuite/study/server/utils/wiremock/WireMockStubs.java @@ -173,7 +173,7 @@ public void verifyNetworkElementsIdsPost(UUID stubUuid, String networkUuid, Stri } public UUID stubRootNetworkTagsAllowed() { - return wireMock.stubFor(WireMock.get(WireMock.urlPathEqualTo(ROOT_NETWORK_TAGS_AUTHORIZED_URL)) + return wireMock.stubFor(WireMock.post(WireMock.urlPathEqualTo(ROOT_NETWORK_TAGS_AUTHORIZED_URL)) .willReturn(WireMock.ok()) ).getId(); } From c08ee299f9b65f04f7b3ec6ff6e737d2b41163ab Mon Sep 17 00:00:00 2001 From: Florent MILLOT Date: Thu, 22 Oct 2026 12:13:56 +0200 Subject: [PATCH 09/12] Send the root network tags check in the request body Signed-off-by: Florent MILLOT --- .../server/dto/RootNetworkTagsCheckInfos.java | 17 +++++++++++++++++ .../NetworkModificationRestService.java | 13 +++++-------- .../study/server/NetworkModificationTest.java | 7 ++++--- .../NetworkModificationServiceTest.java | 13 ++++++++----- .../studycontroller/NodeControllerTest.java | 8 ++++---- .../gridsuite/study/server/utils/TestUtils.java | 4 ++-- .../server/utils/wiremock/WireMockStubs.java | 2 +- 7 files changed, 41 insertions(+), 23 deletions(-) create mode 100644 src/main/java/org/gridsuite/study/server/dto/RootNetworkTagsCheckInfos.java diff --git a/src/main/java/org/gridsuite/study/server/dto/RootNetworkTagsCheckInfos.java b/src/main/java/org/gridsuite/study/server/dto/RootNetworkTagsCheckInfos.java new file mode 100644 index 000000000..19ad5536c --- /dev/null +++ b/src/main/java/org/gridsuite/study/server/dto/RootNetworkTagsCheckInfos.java @@ -0,0 +1,17 @@ +/** + * Copyright (c) 2026, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.gridsuite.study.server.dto; + +import java.util.List; +import java.util.UUID; + +/** + * Modifications to check against root network tags. + */ +public record RootNetworkTagsCheckInfos(List containerUuids, List modificationUuids, List sharedUuids, + List tags) { +} diff --git a/src/main/java/org/gridsuite/study/server/service/networkmodification/NetworkModificationRestService.java b/src/main/java/org/gridsuite/study/server/service/networkmodification/NetworkModificationRestService.java index 5f43d01e5..97e27676c 100644 --- a/src/main/java/org/gridsuite/study/server/service/networkmodification/NetworkModificationRestService.java +++ b/src/main/java/org/gridsuite/study/server/service/networkmodification/NetworkModificationRestService.java @@ -14,6 +14,7 @@ import org.gridsuite.study.server.dto.BuildInfos; import org.gridsuite.study.server.dto.ModificationReference; import org.gridsuite.study.server.dto.NodeReceiver; +import org.gridsuite.study.server.dto.RootNetworkTagsCheckInfos; import org.gridsuite.study.server.dto.SharedModificationsWriteAccess; import org.gridsuite.study.server.dto.modification.*; import org.gridsuite.study.server.dto.workflow.AbstractWorkflowInfos; @@ -764,14 +765,10 @@ public void assertRootNetworkTagsAllowed(List containerUuids, List m if (rootNetworkTags.isEmpty() || containerUuids.isEmpty() && modificationUuids.isEmpty() && sharedUuids.isEmpty()) { return; } - String path = UriComponentsBuilder.fromPath(CONTAINERS + DELIMITER + "root-network-tags/authorized") - .queryParam("containerUuids", containerUuids) - .queryParam("modificationUuids", modificationUuids) - .queryParam("sharedUuids", sharedUuids) - .queryParam("tags", rootNetworkTags) - .build().toUriString(); - - restTemplate.exchange(getNetworkModificationServerURI(false) + path, HttpMethod.GET, userIdEntity(userId), Void.class); + restTemplate.exchange(getNetworkModificationServerURI(false) + CONTAINERS + DELIMITER + "root-network-tags/authorized", + HttpMethod.POST, + userIdEntity(userId, new RootNetworkTagsCheckInfos(containerUuids, modificationUuids, sharedUuids, rootNetworkTags)), + Void.class); } /** diff --git a/src/test/java/org/gridsuite/study/server/NetworkModificationTest.java b/src/test/java/org/gridsuite/study/server/NetworkModificationTest.java index 460764710..f501509f5 100644 --- a/src/test/java/org/gridsuite/study/server/NetworkModificationTest.java +++ b/src/test/java/org/gridsuite/study/server/NetworkModificationTest.java @@ -2370,7 +2370,7 @@ void testPasteForbiddenByASharedModificationNotAssociatedWithTheRootNetworkTags( .businessErrorCode("modification.rootNetworkTagsForbiddenByShared") .businessErrorValues(Map.of("allowedTags", "A, B", "allowedTagsCount", 2)) .build()); - UUID forbiddenStubId = wireMockServer.stubFor(WireMock.get(WireMock.urlPathEqualTo(WireMockStubs.ROOT_NETWORK_TAGS_AUTHORIZED_URL)) + UUID forbiddenStubId = wireMockServer.stubFor(WireMock.post(WireMock.urlPathEqualTo(WireMockStubs.ROOT_NETWORK_TAGS_AUTHORIZED_URL)) .atPriority(1) .willReturn(WireMock.forbidden().withHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_PROBLEM_JSON_VALUE).withBody(forbiddenBody))) .getId(); @@ -2384,8 +2384,9 @@ void testPasteForbiddenByASharedModificationNotAssociatedWithTheRootNetworkTags( .andExpect(jsonPath("$.businessErrorCode").value("modification.rootNetworkTagsForbiddenByShared")) .andExpect(jsonPath("$.businessErrorValues.allowedTags").value("A, B")); - WireMockUtils.verifyGetRequest(wireMockServer, forbiddenStubId, WireMockStubs.ROOT_NETWORK_TAGS_AUTHORIZED_URL, - Map.of("modificationUuids", WireMock.equalTo(modificationUuid.toString()), "tags", WireMock.equalTo(studyEntity.getFirstRootNetwork().getTag()))); + WireMockUtils.verifyPostRequest(wireMockServer, forbiddenStubId, WireMockStubs.ROOT_NETWORK_TAGS_AUTHORIZED_URL, false, Map.of(), + mapper.writeValueAsString(new RootNetworkTagsCheckInfos(List.of(), List.of(modificationUuid), List.of(), + List.of(studyEntity.getFirstRootNetwork().getTag())))); } @Test diff --git a/src/test/java/org/gridsuite/study/server/service/networkmodification/NetworkModificationServiceTest.java b/src/test/java/org/gridsuite/study/server/service/networkmodification/NetworkModificationServiceTest.java index e83d3a40d..d0076e3ed 100644 --- a/src/test/java/org/gridsuite/study/server/service/networkmodification/NetworkModificationServiceTest.java +++ b/src/test/java/org/gridsuite/study/server/service/networkmodification/NetworkModificationServiceTest.java @@ -9,6 +9,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import org.gridsuite.study.server.RemoteServicesProperties; import org.gridsuite.study.server.dto.ModificationReference; +import org.gridsuite.study.server.dto.RootNetworkTagsCheckInfos; import org.gridsuite.study.server.dto.SharedModificationsWriteAccess; import org.gridsuite.study.server.service.RootNetworkService; import org.junit.jupiter.api.BeforeEach; @@ -265,17 +266,19 @@ void testAssertRootNetworkTagsAllowed() { UUID groupUuid = UUID.randomUUID(); UUID modificationUuid = UUID.randomUUID(); UUID sharedUuid = UUID.randomUUID(); + String url = NETWORK_MODIFICATION_SERVER_URI + "/v1/containers/root-network-tags/authorized"; HttpHeaders headers = new HttpHeaders(); headers.set(HEADER_USER_ID, USER_ID); + headers.setContentType(MediaType.APPLICATION_JSON); networkModificationService.assertRootNetworkTagsAllowed(List.of(groupUuid), List.of(), List.of(), List.of("PH1", "PH2"), USER_ID); - verify(restTemplate).exchange(NETWORK_MODIFICATION_SERVER_URI + "/v1/containers/root-network-tags/authorized?containerUuids=" + groupUuid - + "&modificationUuids&sharedUuids&tags=PH1&tags=PH2", HttpMethod.GET, new HttpEntity<>(headers), Void.class); + verify(restTemplate).exchange(url, HttpMethod.POST, new HttpEntity<>( + new RootNetworkTagsCheckInfos(List.of(groupUuid), List.of(), List.of(), List.of("PH1", "PH2")), headers), Void.class); - // the modifications and the shared modifications are checked too, an empty list is sent by its name only + // the modifications and the shared modifications are checked too networkModificationService.assertRootNetworkTagsAllowed(List.of(), List.of(modificationUuid), List.of(sharedUuid), List.of("PH1"), USER_ID); - verify(restTemplate).exchange(NETWORK_MODIFICATION_SERVER_URI + "/v1/containers/root-network-tags/authorized?containerUuids&modificationUuids=" + modificationUuid - + "&sharedUuids=" + sharedUuid + "&tags=PH1", HttpMethod.GET, new HttpEntity<>(headers), Void.class); + verify(restTemplate).exchange(url, HttpMethod.POST, new HttpEntity<>( + new RootNetworkTagsCheckInfos(List.of(), List.of(modificationUuid), List.of(sharedUuid), List.of("PH1")), headers), Void.class); // there is nothing to check without a tag, or without anything to check it on networkModificationService.assertRootNetworkTagsAllowed(List.of(groupUuid), List.of(), List.of(), List.of(), USER_ID); diff --git a/src/test/java/org/gridsuite/study/server/studycontroller/NodeControllerTest.java b/src/test/java/org/gridsuite/study/server/studycontroller/NodeControllerTest.java index 596e286b1..d3376edbb 100644 --- a/src/test/java/org/gridsuite/study/server/studycontroller/NodeControllerTest.java +++ b/src/test/java/org/gridsuite/study/server/studycontroller/NodeControllerTest.java @@ -656,7 +656,7 @@ void testDuplicateNode() throws Exception { assertEquals(BuildStatus.BUILT, networkModificationTreeService.getNodeBuildStatus(node3.getId(), rootNetworkUuid).getGlobalBuildStatus()); // copies within the study are not checked against its shared modifications - WireMockUtilsCriteria.verifyGetRequest(wireMockServer, WireMockStubs.ROOT_NETWORK_TAGS_AUTHORIZED_URL, Map.of(), 0); + WireMockUtilsCriteria.verifyPostRequest(wireMockServer, WireMockStubs.ROOT_NETWORK_TAGS_AUTHORIZED_URL, Map.of(), 0); } @Test @@ -767,7 +767,7 @@ void testDuplicateSubtree() throws Exception { checkElementUpdatedMessageSent(study1Uuid, userId); wireMockStubs.verifyDuplicateModificationGroup(stubDuplicateUuid, 3); // copies within the study are not checked against its shared modifications - WireMockUtilsCriteria.verifyGetRequest(wireMockServer, WireMockStubs.ROOT_NETWORK_TAGS_AUTHORIZED_URL, Map.of(), 0); + WireMockUtilsCriteria.verifyPostRequest(wireMockServer, WireMockStubs.ROOT_NETWORK_TAGS_AUTHORIZED_URL, Map.of(), 0); Map allNodesInfos = networkModificationTreeService.getAllStudyNodesByUuid(study1Uuid); networkModificationTreeService.getAllChildrenUuids(node4.getId()).forEach(childUuid -> @@ -886,8 +886,8 @@ void testDuplicateNodeBetweenStudies() throws Exception { // duplicate the node1 from study 1 after node2 from study 2 UUID duplicatedNodeUuid = duplicateNode(study1Uuid, study2Uuid, node1, study2Node2.getId(), InsertMode.AFTER, true, userId); - WireMockUtilsCriteria.verifyGetRequest(wireMockServer, WireMockStubs.ROOT_NETWORK_TAGS_AUTHORIZED_URL, - Map.of("containerUuids", WireMock.equalTo(node1.getModificationGroupUuid().toString())), 1); + wireMockServer.verify(1, WireMock.postRequestedFor(WireMock.urlPathEqualTo(WireMockStubs.ROOT_NETWORK_TAGS_AUTHORIZED_URL)) + .withRequestBody(WireMock.matchingJsonPath("$.containerUuids[0]", WireMock.equalTo(node1.getModificationGroupUuid().toString())))); //node2 should now have 1 child allNodes = networkModificationTreeService.getAllNodes(study2Uuid); diff --git a/src/test/java/org/gridsuite/study/server/utils/TestUtils.java b/src/test/java/org/gridsuite/study/server/utils/TestUtils.java index 83ff20e29..4b8be7e2d 100644 --- a/src/test/java/org/gridsuite/study/server/utils/TestUtils.java +++ b/src/test/java/org/gridsuite/study/server/utils/TestUtils.java @@ -386,7 +386,7 @@ public static void assertWiremockServerRequestsEmptyThenShutdown(WireMockServer wireMockServer.removeServeEventsMatching(WireMock.postRequestedFor(WireMock.urlPathMatching(UserAdminServerStubs.QUOTA_CONSUME_URL_PATTERN)).build()); wireMockServer.removeServeEventsMatching(WireMock.postRequestedFor(WireMock.urlPathMatching(UserAdminServerStubs.QUOTA_RELEASE_URL_PATTERN)).build()); // Remove the root network tags checks on the shared modifications, auto-stubbed as well - wireMockServer.removeServeEventsMatching(WireMock.getRequestedFor(WireMock.urlPathEqualTo(WireMockStubs.ROOT_NETWORK_TAGS_AUTHORIZED_URL)).build()); + wireMockServer.removeServeEventsMatching(WireMock.postRequestedFor(WireMock.urlPathEqualTo(WireMockStubs.ROOT_NETWORK_TAGS_AUTHORIZED_URL)).build()); var requests = wireMockServer.findAll(WireMock.anyRequestedFor(WireMock.anyUrl())); assertEquals(0, requests.size(), "Unverified WireMock requests found:\n" + requests.stream() .map(r -> "URL: " + r.getUrl() + ", Method: " + r.getMethod() + ", Body: " + r.getBodyAsString() + ", Params: " + r.getQueryParams()) @@ -403,7 +403,7 @@ public static void assertWiremockServerRequestsEmptyThenClear(WireMockServer wir wireMockServer.removeServeEventsMatching(WireMock.postRequestedFor(WireMock.urlPathMatching(UserAdminServerStubs.QUOTA_CONSUME_URL_PATTERN)).build()); wireMockServer.removeServeEventsMatching(WireMock.postRequestedFor(WireMock.urlPathMatching(UserAdminServerStubs.QUOTA_RELEASE_URL_PATTERN)).build()); // Remove the root network tags checks on the shared modifications, auto-stubbed as well - wireMockServer.removeServeEventsMatching(WireMock.getRequestedFor(WireMock.urlPathEqualTo(WireMockStubs.ROOT_NETWORK_TAGS_AUTHORIZED_URL)).build()); + wireMockServer.removeServeEventsMatching(WireMock.postRequestedFor(WireMock.urlPathEqualTo(WireMockStubs.ROOT_NETWORK_TAGS_AUTHORIZED_URL)).build()); assertEquals(0, wireMockServer.findAll(WireMock.anyRequestedFor(WireMock.anyUrl())).size()); } finally { wireMockServer.resetAll(); diff --git a/src/test/java/org/gridsuite/study/server/utils/wiremock/WireMockStubs.java b/src/test/java/org/gridsuite/study/server/utils/wiremock/WireMockStubs.java index 9e2d71e58..010c367af 100644 --- a/src/test/java/org/gridsuite/study/server/utils/wiremock/WireMockStubs.java +++ b/src/test/java/org/gridsuite/study/server/utils/wiremock/WireMockStubs.java @@ -173,7 +173,7 @@ public void verifyNetworkElementsIdsPost(UUID stubUuid, String networkUuid, Stri } public UUID stubRootNetworkTagsAllowed() { - return wireMock.stubFor(WireMock.get(WireMock.urlPathEqualTo(ROOT_NETWORK_TAGS_AUTHORIZED_URL)) + return wireMock.stubFor(WireMock.post(WireMock.urlPathEqualTo(ROOT_NETWORK_TAGS_AUTHORIZED_URL)) .willReturn(WireMock.ok()) ).getId(); } From 672a42c2c84364949992f0a758dbe222f2a9d469 Mon Sep 17 00:00:00 2001 From: Florent MILLOT Date: Thu, 22 Oct 2026 12:26:50 +0200 Subject: [PATCH 10/12] Cover the checks of subtree copies and composite splits Signed-off-by: Florent MILLOT --- .../study/server/service/StudyService.java | 6 ++--- .../study/server/NetworkModificationTest.java | 23 +++++++++++++++++++ .../NetworkModificationServiceTest.java | 9 +++++--- .../studycontroller/NodeControllerTest.java | 18 +++++++++++++++ 4 files changed, 50 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/gridsuite/study/server/service/StudyService.java b/src/main/java/org/gridsuite/study/server/service/StudyService.java index 6625ee6a4..18b33c043 100644 --- a/src/main/java/org/gridsuite/study/server/service/StudyService.java +++ b/src/main/java/org/gridsuite/study/server/service/StudyService.java @@ -2019,9 +2019,9 @@ public void insertCompositeNetworkModifications( /** * Only an INSERT of a composite marked as shared puts a reference to it in the study: the study then uses the shared - * composite itself, which is checked, along with its content. Any other composite (COPY, SPLIT, or INSERT not marked as - * shared) is copied: the study only gets its content, whose references to shared modifications are checked, the - * composite being passed as the container of that content. + * composite itself, which is checked, along with its content. Any other composite (SPLIT, or INSERT not marked as shared) + * is copied: the study only gets its content, whose references to shared modifications are checked, the composite being + * passed as the container of that content. */ private void assertCanWriteCompositesInStudy(UUID studyUuid, List compositesInfos, boolean isInsert, String userId) { Map> uuidsByShared = compositesInfos.stream() diff --git a/src/test/java/org/gridsuite/study/server/NetworkModificationTest.java b/src/test/java/org/gridsuite/study/server/NetworkModificationTest.java index f501509f5..9bc0e2d3e 100644 --- a/src/test/java/org/gridsuite/study/server/NetworkModificationTest.java +++ b/src/test/java/org/gridsuite/study/server/NetworkModificationTest.java @@ -2178,6 +2178,29 @@ void testInsertComposite() throws Exception { "/v1/elements/" + sharedNetModId + "/references", Map.of(), mapper.writeValueAsString(ReferenceAttributes.createReferenceAttributes(insertedReferenceUuid, studyUuid, nodeUuid1, STUDY_NODE))); + // the shared composite is checked itself, the other one only through its content + List studyTags = List.of(studyEntity.getFirstRootNetwork().getTag()); + WireMockUtilsCriteria.verifyPostRequest(wireMockServer, WireMockStubs.ROOT_NETWORK_TAGS_AUTHORIZED_URL, false, Map.of(), + mapper.writeValueAsString(new RootNetworkTagsCheckInfos(List.of(modification1.id()), List.of(), List.of(sharedNetModId), studyTags))); + + // split composites are all checked only through their content, and the study gets no reference to them + wireMockServer.stubFor(WireMock.any(urlPathMatching("/v1/network-composite-modifications/groups/.*")) + .withQueryParam("action", equalTo("SPLIT")) + .willReturn(WireMock.ok() + .withBody(mapper.writeValueAsString(new NetworkModificationsResult(List.of(UUID.randomUUID(), UUID.randomUUID()), List.of(Optional.empty())))) + .withHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE))); + mockMvc.perform(put("/v1/studies/{studyUuid}/nodes/{nodeUuid}/composite-modifications?action=SPLIT", + studyUuid, nodeUuid1) + .contentType(MediaType.APPLICATION_JSON) + .content(compositesData) + .header(USER_ID_HEADER, "userId")) + .andExpect(status().isOk()); + checkUpdateStatusMessagesReceived(studyUuid, nodeUuid1, output); + checkEquipmentUpdatingFinishedMessagesReceived(studyUuid, nodeUuid1); + checkElementUpdatedMessageSent(studyUuid, userId); + WireMockUtilsCriteria.verifyPutRequest(wireMockServer, url, false, Map.of("action", equalTo("SPLIT")), expectedBody); + WireMockUtilsCriteria.verifyPostRequest(wireMockServer, WireMockStubs.ROOT_NETWORK_TAGS_AUTHORIZED_URL, false, Map.of(), + mapper.writeValueAsString(new RootNetworkTagsCheckInfos(List.of(modification1.id(), sharedNetModId), List.of(), List.of(), studyTags))); } @Test diff --git a/src/test/java/org/gridsuite/study/server/service/networkmodification/NetworkModificationServiceTest.java b/src/test/java/org/gridsuite/study/server/service/networkmodification/NetworkModificationServiceTest.java index d0076e3ed..48bfa637d 100644 --- a/src/test/java/org/gridsuite/study/server/service/networkmodification/NetworkModificationServiceTest.java +++ b/src/test/java/org/gridsuite/study/server/service/networkmodification/NetworkModificationServiceTest.java @@ -275,10 +275,13 @@ void testAssertRootNetworkTagsAllowed() { verify(restTemplate).exchange(url, HttpMethod.POST, new HttpEntity<>( new RootNetworkTagsCheckInfos(List.of(groupUuid), List.of(), List.of(), List.of("PH1", "PH2")), headers), Void.class); - // the modifications and the shared modifications are checked too - networkModificationService.assertRootNetworkTagsAllowed(List.of(), List.of(modificationUuid), List.of(sharedUuid), List.of("PH1"), USER_ID); + // the modifications and the shared modifications are checked too, even alone + networkModificationService.assertRootNetworkTagsAllowed(List.of(), List.of(modificationUuid), List.of(), List.of("PH1"), USER_ID); verify(restTemplate).exchange(url, HttpMethod.POST, new HttpEntity<>( - new RootNetworkTagsCheckInfos(List.of(), List.of(modificationUuid), List.of(sharedUuid), List.of("PH1")), headers), Void.class); + new RootNetworkTagsCheckInfos(List.of(), List.of(modificationUuid), List.of(), List.of("PH1")), headers), Void.class); + networkModificationService.assertRootNetworkTagsAllowed(List.of(), List.of(), List.of(sharedUuid), List.of("PH1"), USER_ID); + verify(restTemplate).exchange(url, HttpMethod.POST, new HttpEntity<>( + new RootNetworkTagsCheckInfos(List.of(), List.of(), List.of(sharedUuid), List.of("PH1")), headers), Void.class); // there is nothing to check without a tag, or without anything to check it on networkModificationService.assertRootNetworkTagsAllowed(List.of(groupUuid), List.of(), List.of(), List.of(), USER_ID); diff --git a/src/test/java/org/gridsuite/study/server/studycontroller/NodeControllerTest.java b/src/test/java/org/gridsuite/study/server/studycontroller/NodeControllerTest.java index d3376edbb..2c945fe87 100644 --- a/src/test/java/org/gridsuite/study/server/studycontroller/NodeControllerTest.java +++ b/src/test/java/org/gridsuite/study/server/studycontroller/NodeControllerTest.java @@ -896,6 +896,24 @@ void testDuplicateNodeBetweenStudies() throws Exception { && nodeEntity.getIdNode().equals(duplicatedNodeUuid) && nodeEntity.getParentNode().getIdNode().equals(study2Node2.getId())) .count()); + + // copy the subtree of node1 and node2 from study 1 under the first node of study 2: all its groups are checked + List study2NodesBeforeSubtreeCopy = networkModificationTreeService.getAllNodes(study2Uuid).stream().map(NodeEntity::getIdNode).toList(); + UUID stubDuplicateUuid = wireMockStubs.stubDuplicateModificationGroup(); + mockMvc.perform(post(STUDIES_URL + + "/{targetStudyUuid}/tree/subtrees?subtreeToCopyParentNodeUuid={parentNodeToCopy}&referenceNodeUuid={referenceNodeUuid}&sourceStudyUuid={sourceStudyUuid}", + study2Uuid, modificationNodeUuid, study2ModificationNodeUuid, study1Uuid) + .header(USER_ID_HEADER, userId)) + .andExpect(status().isOk()); + List copiedNodeUuids = networkModificationTreeService.getAllNodes(study2Uuid).stream().map(NodeEntity::getIdNode) + .filter(nodeUuid -> !study2NodesBeforeSubtreeCopy.contains(nodeUuid)).toList(); + assertEquals(3, copiedNodeUuids.size()); + checkSubtreeCreatedMessageSent(study2Uuid, copiedNodeUuids.getFirst(), study2ModificationNodeUuid); + checkElementUpdatedMessageSent(study2Uuid, userId); + wireMockStubs.verifyDuplicateModificationGroup(stubDuplicateUuid, 3); + wireMockServer.verify(1, WireMock.postRequestedFor(WireMock.urlPathEqualTo(WireMockStubs.ROOT_NETWORK_TAGS_AUTHORIZED_URL)) + .withRequestBody(WireMock.matchingJsonPath("$.containerUuids[?(@ == '" + node1.getModificationGroupUuid() + "')]")) + .withRequestBody(WireMock.matchingJsonPath("$.containerUuids[?(@ == '" + node2.getModificationGroupUuid() + "')]"))); } private UUID duplicateNode(UUID sourceStudyUuid, UUID targetStudyUuid, NetworkModificationNode nodeToCopy, UUID referenceNodeUuid, InsertMode insertMode, boolean checkMessagesForStatusModels, From 1dcac44f430db79ae0dd5124c999b4f53996a6b4 Mon Sep 17 00:00:00 2001 From: Florent MILLOT Date: Thu, 22 Oct 2026 15:05:37 +0200 Subject: [PATCH 11/12] Shorten the root network tags check comment Signed-off-by: Florent MILLOT --- .../networkmodification/NetworkModificationRestService.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/org/gridsuite/study/server/service/networkmodification/NetworkModificationRestService.java b/src/main/java/org/gridsuite/study/server/service/networkmodification/NetworkModificationRestService.java index cf3a1c799..0507250e7 100644 --- a/src/main/java/org/gridsuite/study/server/service/networkmodification/NetworkModificationRestService.java +++ b/src/main/java/org/gridsuite/study/server/service/networkmodification/NetworkModificationRestService.java @@ -756,8 +756,7 @@ public SharedModificationsWriteAccess getReferencedModificationsWriteAccess(List } /** - * Asserts that the shared modifications the user cannot write on, among the ones the given containers point to, are - * all associated with the given root network tags. Throws a forbidden error when they are not. + * Throws forbidden when the unwritable shared modifications involved miss some of the tags. */ public void assertRootNetworkTagsAllowed(List containerUuids, List rootNetworkTags, String userId) { if (rootNetworkTags.isEmpty() || containerUuids.isEmpty()) { From 9d690b53f81a2f6aecbf2d6881468dcab9393ac0 Mon Sep 17 00:00:00 2001 From: Florent MILLOT Date: Thu, 22 Oct 2026 15:16:20 +0200 Subject: [PATCH 12/12] Remove or shorten comments restating the code Signed-off-by: Florent MILLOT --- .../server/service/NetworkModificationTreeService.java | 3 --- .../org/gridsuite/study/server/service/StudyService.java | 6 ++---- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/gridsuite/study/server/service/NetworkModificationTreeService.java b/src/main/java/org/gridsuite/study/server/service/NetworkModificationTreeService.java index caa2ba83e..f1d398393 100644 --- a/src/main/java/org/gridsuite/study/server/service/NetworkModificationTreeService.java +++ b/src/main/java/org/gridsuite/study/server/service/NetworkModificationTreeService.java @@ -823,9 +823,6 @@ public UUID getModificationGroupUuid(UUID nodeUuid) { return getNetworkModificationNodeInfoEntity(nodeUuid).getModificationGroupUuid(); } - /** - * @return the modification groups of the given node and of all its descendants - */ @Transactional(readOnly = true) public List getSubtreeModificationGroupUuids(UUID parentNodeUuid) { List nodeUuids = Stream.concat(Stream.of(parentNodeUuid), getAllChildrenUuids(parentNodeUuid).stream()).toList(); diff --git a/src/main/java/org/gridsuite/study/server/service/StudyService.java b/src/main/java/org/gridsuite/study/server/service/StudyService.java index 18b33c043..ef3609db3 100644 --- a/src/main/java/org/gridsuite/study/server/service/StudyService.java +++ b/src/main/java/org/gridsuite/study/server/service/StudyService.java @@ -2018,10 +2018,8 @@ public void insertCompositeNetworkModifications( } /** - * Only an INSERT of a composite marked as shared puts a reference to it in the study: the study then uses the shared - * composite itself, which is checked, along with its content. Any other composite (SPLIT, or INSERT not marked as shared) - * is copied: the study only gets its content, whose references to shared modifications are checked, the composite being - * passed as the container of that content. + * Only an INSERT of a composite marked as shared gives the study a reference to it: the shared composite is checked + * itself. Any other composite is copied: only its content is checked, the composite being passed as its container. */ private void assertCanWriteCompositesInStudy(UUID studyUuid, List compositesInfos, boolean isInsert, String userId) { Map> uuidsByShared = compositesInfos.stream()