Commit be9d595e authored by Nicola Fulvio Calabria's avatar Nicola Fulvio Calabria
Browse files

Added LinkNodes validation to CreateNodeController

parent d86824c8
......@@ -6,6 +6,9 @@
package it.inaf.oats.vospace;
import it.inaf.ia2.aa.data.User;
import it.inaf.oats.vospace.exception.InvalidArgumentException;
import it.inaf.oats.vospace.exception.InvalidURIException;
import net.ivoa.xml.vospace.v2.LinkNode;
import net.ivoa.xml.vospace.v2.Node;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.RequestBody;
......@@ -14,6 +17,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PutMapping;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
@RestController
public class CreateNodeController extends BaseNodeController {
......@@ -23,6 +27,9 @@ public class CreateNodeController extends BaseNodeController {
@Autowired
private CreateNodeService createNodeService;
@Value("${vospace-authority}")
private String authority;
@PutMapping(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})
......@@ -32,8 +39,36 @@ public class CreateNodeController extends BaseNodeController {
LOG.debug("createNodeController called for node with URI {} and PATH {}", node.getUri(), path);
// Get Node path (and validates it too)
String decodedURIPathFromNode = URIUtils.returnVosPathFromNodeURI(node.getUri(), authority);
LOG.debug("createNodeController URI: {} decoded as {}", node.getUri(), decodedURIPathFromNode);
// Check if payload URI is consistent with http request
if (!decodedURIPathFromNode.equals(path)) {
throw new InvalidURIException(decodedURIPathFromNode, path);
}
// validate format of input node
this.validateInputNode(node);
return createNodeService.createNode(node, path, principal);
}
private void validateInputNode(Node node) {
if (node instanceof LinkNode) {
LinkNode linkNode = (LinkNode) node;
String target = linkNode.getTarget();
// I validate it here to add context easily
if (target == null) {
throw new InvalidArgumentException("LinkNode in payload has no target element specified");
}
URIUtils.returnVosPathFromNodeURI(linkNode.getTarget(), authority);
}
}
}
......@@ -10,7 +10,6 @@ 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.DuplicateNodeException;
import it.inaf.oats.vospace.exception.InvalidURIException;
import it.inaf.oats.vospace.exception.LinkFoundException;
import it.inaf.oats.vospace.exception.PermissionDeniedException;
import it.inaf.oats.vospace.persistence.NodeDAO;
......@@ -19,7 +18,6 @@ import net.ivoa.xml.vospace.v2.Property;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.EnableTransactionManagement;
......@@ -30,25 +28,12 @@ public class CreateNodeService {
@Autowired
private NodeDAO nodeDao;
@Value("${vospace-authority}")
private String authority;
private static final Logger LOG = LoggerFactory.getLogger(CreateNodeService.class);
public Node createNode(Node node, String path, User principal) {
LOG.debug("createNodeService called for node with URI {} and PATH {}", node.getUri(), path);
// Get Node path (and validates it too)
String decodedURIPathFromNode = URIUtils.returnVosPathFromNodeURI(node.getUri(), authority);
LOG.debug("createNodeService URI: {} decoded as {}", node.getUri(), decodedURIPathFromNode);
// Check if payload URI is consistent with http request
if (!decodedURIPathFromNode.equals(path)) {
throw new InvalidURIException(decodedURIPathFromNode, path);
}
// Check if another node is already present at specified path
// This checks if the user is trying to insert the root node at "/" too
if (nodeDao.listNode(path).isPresent()) {
......
......@@ -51,6 +51,10 @@ public class URIUtils {
String resultPath = null;
try {
if(nodeURI == null)
throw new IllegalArgumentException("URI string is null");
URI uri = new URI(nodeURI);
// Check scheme
......@@ -88,7 +92,7 @@ public class URIUtils {
} catch (URISyntaxException e) {
throw new InvalidURIException(nodeURI);
}
}
return resultPath;
......
......@@ -30,6 +30,7 @@ import org.springframework.boot.test.mock.mockito.SpyBean;
import net.ivoa.xml.vospace.v2.ContainerNode;
import net.ivoa.xml.vospace.v2.LinkNode;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
......@@ -44,17 +45,17 @@ import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMock
@TestPropertySource(properties = "spring.main.allow-bean-definition-overriding=true")
@AutoConfigureMockMvc
public class CreateNodeControllerTest {
@MockBean
private NodeDAO nodeDao;
@SpyBean
@Autowired
private CreateNodeController controller;
@Autowired
private MockMvc mockMvc;
private ContainerNode getContainerParentNode(String path) {
ContainerNode parentNode = new ContainerNode();
// Set parent node address at /
......@@ -66,7 +67,7 @@ public class CreateNodeControllerTest {
parentNode.setProperties(List.of(groups));
return parentNode;
}
private ContainerNode getContainerParentNodeWithCreator(String path) {
ContainerNode parentNode = new ContainerNode();
// Set parent node address at /
......@@ -77,7 +78,7 @@ public class CreateNodeControllerTest {
parentNode.setProperties(List.of(creator));
return parentNode;
}
private LinkNode getLinkParentNode(String path) {
LinkNode parentNode = new LinkNode();
// Set parent node address at /
......@@ -89,15 +90,15 @@ public class CreateNodeControllerTest {
parentNode.setProperties(List.of(groups));
return parentNode;
}
@Test
public void testFromJsonToXml() throws Exception {
String requestBody
= getResourceFileContent("create-unstructured-data-node.json");
when(nodeDao.listNode(eq("/")))
.thenReturn(Optional.of(getContainerParentNode("/")));
mockMvc.perform(put("/nodes/mydata1")
.header("Authorization", "Bearer user2_token")
.content(requestBody)
......@@ -105,17 +106,17 @@ public class CreateNodeControllerTest {
.accept(MediaType.APPLICATION_XML))
.andDo(print())
.andExpect(status().isOk());
verifyArguments();
}
@Test
public void testFromXmlToJson() throws Exception {
String requestBody = getResourceFileContent("create-unstructured-data-node.xml");
when(nodeDao.listNode(eq("/")))
.thenReturn(Optional.of(getContainerParentNode("/")));
mockMvc.perform(put("/nodes/mydata1")
.header("Authorization", "Bearer user2_token")
.content(requestBody)
......@@ -123,18 +124,18 @@ public class CreateNodeControllerTest {
.accept(MediaType.APPLICATION_JSON))
.andDo(print())
.andExpect(status().isOk());
verifyArguments();
verify(nodeDao, times(1)).createNode(any());
}
@Test
public void testFromXmlToXml() throws Exception {
String requestBody = getResourceFileContent("create-unstructured-data-node.xml");
when(nodeDao.listNode(eq("/")))
.thenReturn(Optional.of(getContainerParentNode("/")));
mockMvc.perform(put("/nodes/mydata1")
.header("Authorization", "Bearer user2_token")
.content(requestBody)
......@@ -142,18 +143,18 @@ public class CreateNodeControllerTest {
.accept(MediaType.APPLICATION_XML))
.andDo(print())
.andExpect(status().isOk());
verifyArguments();
verify(nodeDao, times(1)).createNode(any());
}
@Test
public void testFromJsonToJson() throws Exception {
String requestBody = getResourceFileContent("create-unstructured-data-node.json");
when(nodeDao.listNode(eq("/")))
.thenReturn(Optional.of(getContainerParentNode("/")));
mockMvc.perform(put("/nodes/mydata1")
.header("Authorization", "Bearer user2_token")
.content(requestBody)
......@@ -161,18 +162,18 @@ public class CreateNodeControllerTest {
.accept(MediaType.APPLICATION_JSON))
.andDo(print())
.andExpect(status().isOk());
verifyArguments();
verify(nodeDao, times(1)).createNode(any());
}
@Test
public void testURIConsistence() throws Exception {
String requestBody = getResourceFileContent("create-unstructured-data-node.xml");
when(nodeDao.listNode(eq("/")))
.thenReturn(Optional.of(getContainerParentNode("/")));
mockMvc.perform(put("/nodes/mydata2")
.header("Authorization", "Bearer user2_token")
.content(requestBody)
......@@ -180,20 +181,77 @@ public class CreateNodeControllerTest {
.accept(MediaType.APPLICATION_XML))
.andDo(print())
.andExpect(status().is4xxClientError());
verifyArguments();
}
@Test
public void testCreateInternalLinkNode() throws Exception {
String requestBody = getResourceFileContent("create-internal-link-node.xml");
when(nodeDao.listNode(eq("/")))
.thenReturn(Optional.of(getContainerParentNode("/")));
mockMvc.perform(put("/nodes/myInternalLink")
.header("Authorization", "Bearer user2_token")
.content(requestBody)
.contentType(MediaType.APPLICATION_XML)
.accept(MediaType.APPLICATION_XML))
.andDo(print())
.andExpect(status().isOk());
verifyLinkArguments("vos://example.com!vospace/myDummyDataNode1");
}
@Test
public void testCreateExternalLinkNode() throws Exception {
String requestBody = getResourceFileContent("create-external-link-node.xml");
when(nodeDao.listNode(eq("/")))
.thenReturn(Optional.of(getContainerParentNode("/")));
mockMvc.perform(put("/nodes/myExternalLink")
.header("Authorization", "Bearer user2_token")
.content(requestBody)
.contentType(MediaType.APPLICATION_XML)
.accept(MediaType.APPLICATION_XML))
.andDo(print())
.andExpect(status().is4xxClientError());
verifyLinkArguments("vos://external.com!vospace/myDummyDataNode1");
}
@Test
public void testCreateLinkNodeNoTarget() throws Exception {
String requestBody = getResourceFileContent("create-link-node-notarget.xml");
when(nodeDao.listNode(eq("/")))
.thenReturn(Optional.of(getContainerParentNode("/")));
mockMvc.perform(put("/nodes/myNoTargetLink")
.header("Authorization", "Bearer user2_token")
.content(requestBody)
.contentType(MediaType.APPLICATION_XML)
.accept(MediaType.APPLICATION_XML))
.andDo(print())
.andExpect(status().is4xxClientError());
verifyLinkArguments(null);
}
@Test
public void testNodeAlreadyExisting() throws Exception {
String requestBody = getResourceFileContent("create-unstructured-data-node.xml");
when(nodeDao.listNode(eq("/")))
.thenReturn(Optional.of(getContainerParentNode("/")));
when(nodeDao.listNode(eq("/mydata1")))
.thenReturn(Optional.of(getContainerParentNode("/mydata1")));
mockMvc.perform(put("/nodes/mydata1")
.header("Authorization", "Bearer user2_token")
.content(requestBody)
......@@ -201,17 +259,17 @@ public class CreateNodeControllerTest {
.accept(MediaType.APPLICATION_XML))
.andDo(print())
.andExpect(status().is4xxClientError());
verifyArguments();
}
@Test
public void testContainerNotFound() throws Exception {
String requestBody = getResourceFileContent("create-unstructured-data-node.xml");
when(nodeDao.listNode(eq("/")))
.thenReturn(Optional.ofNullable(null));
mockMvc.perform(put("/nodes/mydata1")
.header("Authorization", "Bearer user2_token")
.content(requestBody)
......@@ -219,17 +277,17 @@ public class CreateNodeControllerTest {
.accept(MediaType.APPLICATION_XML))
.andDo(print())
.andExpect(status().is4xxClientError());
verifyArguments();
}
@Test
public void testLinkNodeFound() throws Exception {
String requestBody = getResourceFileContent("create-unstructured-data-node.xml");
when(nodeDao.listNode(eq("/")))
.thenReturn(Optional.of(getLinkParentNode("/")));
mockMvc.perform(put("/nodes/mydata1")
.header("Authorization", "Bearer user2_token")
.content(requestBody)
......@@ -237,17 +295,17 @@ public class CreateNodeControllerTest {
.accept(MediaType.APPLICATION_XML))
.andDo(print())
.andExpect(status().is4xxClientError());
verifyArguments();
}
@Test
public void testPermissionDenied() throws Exception {
String requestBody = getResourceFileContent("create-unstructured-data-node.xml");
when(nodeDao.listNode(eq("/")))
.thenReturn(Optional.of(getContainerParentNode("/")));
mockMvc.perform(put("/nodes/mydata1")
.header("Authorization", "Bearer user1_token")
.content(requestBody)
......@@ -255,17 +313,17 @@ public class CreateNodeControllerTest {
.accept(MediaType.APPLICATION_XML))
.andDo(print())
.andExpect(status().is4xxClientError());
verifyArguments();
}
@Test
public void testWriteWithOnlyOwnership() throws Exception {
String requestBody = getResourceFileContent("create-unstructured-data-node.xml");
when(nodeDao.listNode(eq("/")))
.thenReturn(Optional.of(getContainerParentNodeWithCreator("/")));
mockMvc.perform(put("/nodes/mydata1")
.header("Authorization", "Bearer user2_token")
.content(requestBody)
......@@ -273,16 +331,16 @@ public class CreateNodeControllerTest {
.accept(MediaType.APPLICATION_XML))
.andDo(print())
.andExpect(status().is2xxSuccessful());
verifyArguments();
verify(nodeDao, times(1)).createNode(any());
}
@Test
public void testWriteOwnerAbsent() throws Exception {
String requestBody
= getResourceFileContent("create-unstructured-data-node.xml");
when(nodeDao.listNode(eq("/")))
.thenReturn(Optional.of(getContainerParentNodeWithCreator("/")));
......@@ -300,18 +358,18 @@ public class CreateNodeControllerTest {
UnstructuredDataNode udn = (UnstructuredDataNode) node;
String creator = NodeProperties.getNodePropertyByURI(
udn, NodeProperties.CREATOR_URI);
return (creator != null && creator.equals("user2"));
return (creator != null && creator.equals("user2"));
}));
}
@Test
public void testGroupPropertiesAbsent() throws Exception {
String requestBody
= getResourceFileContent("create-unstructured-data-node.xml");
ContainerNode cdn = getContainerParentNode("/");
when(nodeDao.listNode(eq("/")))
.thenReturn(Optional.of(cdn));
......@@ -331,16 +389,16 @@ public class CreateNodeControllerTest {
udn, NodeProperties.GROUP_READ_URI);
String groupWrite = NodeProperties.getNodePropertyByURI(
udn, NodeProperties.GROUP_WRITE_URI);
return (groupRead == null && groupWrite.equals("group1 group2"));
return (groupRead == null && groupWrite.equals("group1 group2"));
}));
}
@Test
public void testWriteOwnerMismatch() throws Exception {
String requestBody
= getResourceFileContent("create-unstructured-data-node-user1.xml");
when(nodeDao.listNode(eq("/")))
.thenReturn(Optional.of(getContainerParentNodeWithCreator("/")));
......@@ -354,15 +412,15 @@ public class CreateNodeControllerTest {
.andExpect(status().is4xxClientError());
// assert createNode is not called
verify(nodeDao, times(0)).createNode(any());
verify(nodeDao, times(0)).createNode(any());
}
@Test
public void testSubPath() throws Exception {
String requestBody = getResourceFileContent("create-unstructured-data-node.xml")
.replace("/mydata1", "/mydata1/anothernode");
mockMvc.perform(put("/nodes/mydata1/anothernode")
.header("Authorization", "Bearer user2_token")
.content(requestBody)
......@@ -373,19 +431,19 @@ public class CreateNodeControllerTest {
// Using ArgumentCaptor for verifying multiple method invocations
ArgumentCaptor<String> argCaptor = ArgumentCaptor.forClass(String.class);
verify(nodeDao, times(2)).listNode(argCaptor.capture());
assertEquals("/mydata1/anothernode", argCaptor.getAllValues().get(0));
assertEquals("/mydata1", argCaptor.getAllValues().get(1));
}
@Test
public void testIllegalChars() throws Exception {
String requestBody = getResourceFileContent("create-unstructured-data-node.xml")
.replace("/mydata1", "/mydata1/?anothernode");
String message = mockMvc.perform(put(new URI("/nodes/mydata1/%3Fanothernode"))
.header("Authorization", "Bearer user2_token")
.content(requestBody)
......@@ -394,10 +452,10 @@ public class CreateNodeControllerTest {
.andDo(print())
.andExpect(status().isBadRequest())
.andReturn().getResolvedException().getMessage();
assertTrue(message.contains("contains an illegal character"));
}
private void verifyArguments() {
verify(controller).createNode(
argThat(node -> {
......@@ -408,7 +466,19 @@ public class CreateNodeControllerTest {
&& "ivo://ivoa.net/vospace/core#description".equals(property.getUri());
}), any());
}
private void verifyLinkArguments(String target) {
verify(controller).createNode(
argThat(node -> {
LinkNode linkNode = (LinkNode) node;
Property property = linkNode.getProperties().get(0);
return "vos:LinkNode".equals(linkNode.getType())
&& "test value".equals(property.getValue())
&& "ivo://ivoa.net/vospace/core#description".equals(property.getUri())
&& Objects.equals(target, linkNode.getTarget());
}), any());
}
protected static String getResourceFileContent(String fileName) throws Exception {
try (InputStream in = CreateNodeControllerTest.class.getClassLoader().getResourceAsStream(fileName)) {
return new String(in.readAllBytes(), StandardCharsets.UTF_8);
......
......@@ -17,6 +17,7 @@ import java.util.Set;
import javax.sql.DataSource;
import net.ivoa.xml.vospace.v2.ContainerNode;
import net.ivoa.xml.vospace.v2.DataNode;