diff --git a/src/main/java/it/inaf/oats/vospace/DeleteNodeController.java b/src/main/java/it/inaf/oats/vospace/DeleteNodeController.java new file mode 100644 index 0000000000000000000000000000000000000000..54b5661eeac789dd67db965a1dfa127041066f51 --- /dev/null +++ b/src/main/java/it/inaf/oats/vospace/DeleteNodeController.java @@ -0,0 +1,130 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package it.inaf.oats.vospace; + +import it.inaf.ia2.aa.data.User; +import it.inaf.oats.vospace.datamodel.NodeProperties; +import it.inaf.oats.vospace.datamodel.NodeUtils; +import it.inaf.oats.vospace.exception.ContainerNotFoundException; +import it.inaf.oats.vospace.exception.InternalFaultException; +import it.inaf.oats.vospace.exception.LinkFoundException; +import it.inaf.oats.vospace.exception.NodeNotFoundException; +import it.inaf.oats.vospace.exception.PermissionDeniedException; +import it.inaf.oats.vospace.persistence.NodeDAO; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import javax.servlet.http.HttpServletRequest; +import net.ivoa.xml.vospace.v2.Node; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + + +@RestController +public class DeleteNodeController extends BaseNodeController { + + @Autowired + private NodeDAO nodeDAO; + + @GetMapping(value = {"/nodes", "/nodes/**"}, + consumes = {MediaType.APPLICATION_XML_VALUE, MediaType.TEXT_XML_VALUE, MediaType.APPLICATION_JSON_VALUE}, + produces = {MediaType.APPLICATION_XML_VALUE, MediaType.TEXT_XML_VALUE, MediaType.APPLICATION_JSON_VALUE}) + public ResponseEntity<Node> deleteNode(HttpServletRequest request, User principal) { + + String path = getPath(); + + // Check if the node is present, + // if the node does not exist the service SHALL throw a HTTP 404 status code + // including a NodeNotFound fault in the entity-body + // If note present, got it + Node toBeDeletedNode = nodeDAO.listNode(path) + .orElseThrow(() -> new NodeNotFoundException(path)); + + // If a parent node in the URI path is a LinkNode, the service SHALL throw + // a HTTP 400 status code including a LinkFound fault in the entity-body. + // For example, given the URI path /a/b/c, the service must throw a HTTP 400 + // status code including a LinkFound fault in the entity-body if either /a + // or /a/b are LinkNodes. + List<String> pathComponents = NodeUtils.subPathComponents(path); + if (pathComponents.size() == 0) { + + // Manage root node + throw new PermissionDeniedException("root"); + + } else { + + // Manage all precursors in full path + for (int i = 1; i < pathComponents.size(); i++) { + String tmpPath = pathComponents.get(i); + Node mynode = nodeDAO.listNode(tmpPath) + .orElseThrow(() -> new NodeNotFoundException(tmpPath)); + if (mynode.getType().equals("vos:LinkNode") && i < pathComponents.size()-1) // a LinkNode leaf can be deleted + throw new LinkFoundException(tmpPath); + + } + + } + + // DUPLICATED code from CreateNodeController - BEGIN + List<String> nodeOwner + = NodeProperties.getNodePropertyByURI(toBeDeletedNode, NodeProperties.CREATOR_URI); + //= getNodePropertyByURI( + // toBeDeletedNode, "ivo://ivoa.net/vospace/core#creator"); + + + if (nodeOwner == null + || nodeOwner.isEmpty() + || !nodeOwner.get(0).equals(principal.getName())) { + // Node owner check has failed: let's check if user can write + // due to group privileges + + List<String> userGroups = principal.getGroups(); + + // If the user doesn't belong to any groups throw exception + if (userGroups == null || userGroups.isEmpty()) { + throw new PermissionDeniedException(path); + } + + List<String> groupWritePropValues + = NodeProperties.getNodePropertyByURI(toBeDeletedNode, + NodeProperties.CREATOR_URI); + + // If groupwrite property is absent in Parent Node throw exception + if (groupWritePropValues == null + || groupWritePropValues.isEmpty()) { + throw new PermissionDeniedException(path); + } + + List<String> nodeGroups + = NodeProperties.parsePropertyStringToList(groupWritePropValues.get(0)); + + if (nodeGroups.isEmpty() + || !nodeGroups.stream() + .anyMatch((i) -> userGroups.contains(i))) { + throw new PermissionDeniedException(path); + } + + } + + // DUPLICATED code from CreateNodeController - END + + + return ResponseEntity.ok(toBeDeletedNode); + //return toBeDeletedNode; + //return ResponseEntity.ok(nodeDAO.deleteNode(path) + // .orElseThrow(() -> new InternalFaultException(path))); + + } + +} + + + + + diff --git a/src/main/java/it/inaf/oats/vospace/exception/InternalFaultException.java b/src/main/java/it/inaf/oats/vospace/exception/InternalFaultException.java new file mode 100644 index 0000000000000000000000000000000000000000..2429c070a061a09f10ea8eb712c1993847dcd1f8 --- /dev/null +++ b/src/main/java/it/inaf/oats/vospace/exception/InternalFaultException.java @@ -0,0 +1,19 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package it.inaf.oats.vospace.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + + +@ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR) // Status code 500 +public class InternalFaultException extends VoSpaceException { + + public InternalFaultException(String msg) { + super("InternalFaultException: " + msg); + } + +} diff --git a/src/test/java/it/inaf/oats/vospace/DeleteNodeControllerTest.java b/src/test/java/it/inaf/oats/vospace/DeleteNodeControllerTest.java new file mode 100644 index 0000000000000000000000000000000000000000..e9073202da5f59c7ee019ec3dd1d943b2fc2d470 --- /dev/null +++ b/src/test/java/it/inaf/oats/vospace/DeleteNodeControllerTest.java @@ -0,0 +1,207 @@ +package it.inaf.oats.vospace; + +import static it.inaf.oats.vospace.CreateNodeControllerTest.getResourceFileContent; +import it.inaf.oats.vospace.persistence.NodeDAO; +import java.util.Optional; +import net.ivoa.xml.vospace.v2.ContainerNode; +import net.ivoa.xml.vospace.v2.DataNode; +import net.ivoa.xml.vospace.v2.Node; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.web.servlet.MockMvc; +import org.junit.jupiter.api.Test; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@ContextConfiguration(classes = {TokenFilterConfig.class}) +@TestPropertySource(properties = "spring.main.allow-bean-definition-overriding=true") +@AutoConfigureMockMvc +public class DeleteNodeControllerTest { + + private static final String URI_PREFIX = "vos://example.com!vospace"; + + @MockBean + private NodeDAO nodeDao; + + @SpyBean + @Autowired + private DeleteNodeController controller; + + @Autowired + private MockMvc mockMvc; + + + @Test + public void testDeleteRootNode() throws Exception { + + when(nodeDao.listNode(eq("/"))).thenReturn(getRootNode()); + + mockMvc.perform(MockMvcRequestBuilders + .delete("/nodes") + .header("Authorization", "Bearer user2_token")) + .andExpect(status().isForbidden()); + /* + mockMvc.perform(delete("/members/1"). + .header("Authorization", "Bearer user2_token")) + .andExpect(status().isNotFound()); + mockMvc.perform(delete("/") + .header("Authorization", "Bearer user2_token")) + .andExpect(status().isNotFound()); + */ + } + + + @Test + public void testNodeNotFound() throws Exception { + + when(nodeDao.listNode(eq("/mynode"))).thenReturn(Optional.of(getDataNode())); + + mockMvc.perform(MockMvcRequestBuilders + .delete("/nodes/mynode") + .header("Authorization", "Bearer user2_token")) + .andExpect(status().isOk()); + /* + mockMvc.perform(delete("/members/1"). + .header("Authorization", "Bearer user2_token")) + .andExpect(status().isNotFound()); + mockMvc.perform(delete("/") + .header("Authorization", "Bearer user2_token")) + .andExpect(status().isNotFound()); + */ + } + /* + @Test + public void testDeleteRoot1() throws Exception { + + mockMvc.perform(put("/nodes/mydata1") + .header("Authorization", "Bearer user2_token")) + //.content(requestBody) + //.contentType(MediaType.APPLICATION_JSON) + //.accept(MediaType.APPLICATION_XML)) + //.andDo(print()) + .andExpect(status().isOk()); + + + } + + + + @Test + public void testDeleteRoot2() throws Exception { + + mockMvc.perform(put("/nodes/mydata1") + .header("Authorization", "Bearer user2_token")) + //.content(requestBody) + //.contentType(MediaType.APPLICATION_JSON) + //.accept(MediaType.APPLICATION_XML)) + //.andDo(print()) + .andExpect(status().isOk()); + + + } + + + @Test + public void testDeleteExistingNodeInRoot() throws Exception { + + mockMvc.perform(put("/nodes/mydata1") + .header("Authorization", "Bearer user2_token")) + //.content(requestBody) + //.contentType(MediaType.APPLICATION_JSON) + //.accept(MediaType.APPLICATION_XML)) + //.andDo(print()) + .andExpect(status().isOk()); + + + } + + + @Test + public void testDeleteExistingNodeLeaf() throws Exception { + + mockMvc.perform(put("/nodes/mydata1") + .header("Authorization", "Bearer user2_token")) + //.content(requestBody) + //.contentType(MediaType.APPLICATION_JSON) + //.accept(MediaType.APPLICATION_XML)) + //.andDo(print()) + .andExpect(status().isOk()); + + + } + + + @Test + public void testDeleteExistingDataLinkInRoot() throws Exception { + + mockMvc.perform(put("/nodes/mydata1") + .header("Authorization", "Bearer user2_token")) + //.content(requestBody) + //.contentType(MediaType.APPLICATION_JSON) + //.accept(MediaType.APPLICATION_XML)) + //.andDo(print()) + .andExpect(status().isOk()); + + + } + + + @Test + public void testDeleteExistingDataLinkLeaf() throws Exception { + + mockMvc.perform(put("/nodes/mydata1") + .header("Authorization", "Bearer user2_token")) + //.content(requestBody) + //.contentType(MediaType.APPLICATION_JSON) + //.accept(MediaType.APPLICATION_XML)) + //.andDo(print()) + .andExpect(status().isOk()); + + + } + + + @Test + public void testDeleteExistingNodeWithDataLinkInPath() throws Exception { + + mockMvc.perform(put("/nodes/mydata1") + .header("Authorization", "Bearer user2_token")) + //.content(requestBody) + //.contentType(MediaType.APPLICATION_JSON) + //.accept(MediaType.APPLICATION_XML)) + //.andDo(print()) + .andExpect(status().isOk()); + + + } +*/ + + + + private Optional<Node> getRootNode() { + ContainerNode root = new ContainerNode(); + root.setUri(URI_PREFIX + "/"); + root.getNodes().add(getDataNode()); + return Optional.of(root); + } + + + + private Node getDataNode() { + DataNode node = new DataNode(); + node.setUri(URI_PREFIX + "/mynode"); + return node; + } + +}