diff --git a/src/main/java/it/inaf/oats/vospace/datamodel/NodeProperties.java b/src/main/java/it/inaf/oats/vospace/datamodel/NodeProperties.java index 100ba0c9f342c60fcd43eddbcd54ecf6160370d4..fac730e73dce3e82cb1b2fab306f61685f377ed5 100644 --- a/src/main/java/it/inaf/oats/vospace/datamodel/NodeProperties.java +++ b/src/main/java/it/inaf/oats/vospace/datamodel/NodeProperties.java @@ -1,8 +1,3 @@ -/* - * 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.datamodel; import java.util.List; @@ -10,17 +5,11 @@ import java.util.stream.Collectors; import net.ivoa.xml.vospace.v2.Node; import net.ivoa.xml.vospace.v2.Property; -/** - * - * @author bertocco - */ -public class NodeProperties { +public abstract class NodeProperties { private NodeProperties() { } - - private static final String PROPERTY_BASE_URI = "ivo://ivoa.net/vospace/core#"; public static final String AVAILABLE_SPACE_URI = "ivo://ivoa.net/vospace/core#availableSpace"; // the amount of space available within a container public static final String INITIAL_CREATION_TIME_URI = "ivo://ivoa.net/vospace/core#btime"; // the initial creation time public static final String CONTRIBUTOR_URI = "ivo://ivoa.net/vospace/core#contributor"; // an entity responsible for making contributions to this resource @@ -45,14 +34,21 @@ public class NodeProperties { public static final String SUBJECT_URI = "ivo://ivoa.net/vospace/core#subject"; // the topic of the resource public static final String TITLE_URI = "ivo://ivoa.net/vospace/core#title"; // a name given to the resource public static final String TYPE_URI = "ivo://ivoa.net/vospace/core#type"; // the nature or genre of the resource - + // + // Non-standard properties + public static final String ASYNC_TRANS_URN = "urn:async_trans"; + public static final String STICKY_URN = "urn:sticky"; + public static String getStandardNodePropertyByName(Node node, String propertyName) { + return getNodePropertyByURI(node, "ivo://ivoa.net/vospace/core#".concat(propertyName)); + } - public static String getPropertiesStringByName(Node node, String propertyName) { + + public static String getProperty(Node node, String propertyName) { for (Property property : node.getProperties()) { - if (property.getUri().equals(PROPERTY_BASE_URI.concat(propertyName))) { + if (property.getUri().equals("ivo://ivoa.net/vospace/core#".concat(propertyName))) { return property.getValue(); } } @@ -60,7 +56,7 @@ public class NodeProperties { } - public static String getPropertiesStringByURI(Node node, String uri) { + public static String getNodePropertyByURI(Node node, String uri) { for (Property property : node.getProperties()) { if (uri.equals(property.getUri())) { @@ -72,7 +68,7 @@ public class NodeProperties { } // Returns all properties stored inside the node under the requested // property URI. - public static List<String> getNodePropertiesListByURI(Node node, String propertyURI) { + public static List<String> getNodePropertyAsListByURI(Node node, String propertyURI) { List<String> propertyList = node.getProperties().stream() .filter((i) -> i.getUri() @@ -96,10 +92,5 @@ public class NodeProperties { return List.of(trimmedProperty.split(separator)); } - - - public static String getPropertyURI(String propertyName) { - return PROPERTY_BASE_URI.concat(propertyName); - } } diff --git a/src/main/java/it/inaf/oats/vospace/datamodel/NodeUtils.java b/src/main/java/it/inaf/oats/vospace/datamodel/NodeUtils.java index e878de1cb3dd3dad4b5dfc8d292a1203327996e5..77c14b13ae98e74839639db7121b2aa8c89d097d 100644 --- a/src/main/java/it/inaf/oats/vospace/datamodel/NodeUtils.java +++ b/src/main/java/it/inaf/oats/vospace/datamodel/NodeUtils.java @@ -1,8 +1,3 @@ -/* - * 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.datamodel; import java.security.Principal; @@ -11,29 +6,64 @@ import java.util.List; import net.ivoa.xml.vospace.v2.ContainerNode; import net.ivoa.xml.vospace.v2.DataNode; import net.ivoa.xml.vospace.v2.Node; +import java.net.URLDecoder; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.regex.Pattern; +import java.util.stream.Collectors; import net.ivoa.xml.vospace.v2.StructuredDataNode; public class NodeUtils { - - - + + /** + * Forbidden path chars are non printable characters and some symbols that + * could create issues to scripts that manipulates files. Other UTF-8 + * characters are allowed. Front end needs to pay attention to other allowed + * characters like & and parenthesis in any case, also to avoid XSS attacks. + */ + private static final Pattern FORBIDDEN_CHARS = Pattern.compile("[\\x00\\x08\\x0B\\x0C\\x0E-\\x1F" + Pattern.quote("<>?\":\\|/'`*") + "]"); + /** * Slash is a special character in defining REST endpoints and trying to * define a PathVariable containing slashes doesn't work, so the endpoint * has been defined using "/nodes/**" instead of "/nodes/{path}" and the - * path is extracted manually parsing the request URL. + * path is extracted manually parsing the request URL. Proper URL encoding + * handling is needed, considering also that slashes mustn't be escaped. */ - public static String getPathFromRequestURLString(String requestUrlString ) { - - String[] split = requestUrlString.split("/nodes/"); + public static String getPathFromRequestURLString(String requestUrlString) { + return getPathFromRequestURLString(requestUrlString, "/nodes/"); + } + + public static String getPathFromRequestURLString(String requestUrlString, String prefix) { + + String[] split = requestUrlString.split(prefix); String path = "/"; if (split.length == 2) { - path += split[1]; + String[] parts = split[1].split("/"); + path += String.join("/", Arrays.stream(parts) + .map(p -> { + String decoded = URLDecoder.decode(p, StandardCharsets.UTF_8); + if (FORBIDDEN_CHARS.matcher(decoded).find()) { + throw new IllegalArgumentException("Path segment " + decoded + " contains an illegal character"); + } + return decoded; + }) + .collect(Collectors.toList())); } return path; } + + public static String urlEncodePath(String path) { + String[] parts = path.split("/"); + return String.join("/", Arrays.stream(parts) + .map(p -> URLEncoder.encode(p, StandardCharsets.UTF_8).replace("+", "%20")) + .collect(Collectors.toList())); + } // This method assumes that URL is in the format /node1/node2/... // multiple slashes as a single separator are allowed @@ -88,6 +118,8 @@ public class NodeUtils { } + + public static boolean checkIfWritable(Node myNode, String userName, List<String> userGroups) { return checkAccessPropery(myNode, userName, userGroups, NodeProperties.GROUP_WRITE_URI); @@ -144,60 +176,6 @@ public class NodeUtils { } return true; - } - - - public static String getDbNodeType(Node node) { - if (node instanceof ContainerNode) { - return "container"; - } else if (node instanceof DataNode) { - return "data"; - } - throw new UnsupportedOperationException("Unable to retrieve database node type for class " + node.getClass().getCanonicalName()); - } - - - public static String getNodeName(String path) { - String[] parsedPath = path.split("/"); - - return parsedPath[parsedPath.length - 1]; - } - - - public static String getNodeName(Node myNode) { - String uri = myNode.getUri(); - return getNodeName(uri); - } - - - public static boolean getIsBusy(Node myNode) { - - if (myNode instanceof DataNode) { - - DataNode dataNode = (DataNode) myNode; - return dataNode.isBusy(); - } - - return false; - } - - - public static Node getTypedNode(String type) { - Node node; - switch (type) { - case "container": - node = new ContainerNode(); - break; - case "data": - node = new DataNode(); - break; - case "structured": - node = new StructuredDataNode(); - break; - default: - throw new UnsupportedOperationException("Node type " + type + " not supported yet"); - } - return node; - } + } } diff --git a/src/main/java/net/ivoa/xml/uws/v1/JobSummary.java b/src/main/java/net/ivoa/xml/uws/v1/JobSummary.java index b755062d96cd82b22177bb8dbeb968175fac1901..1d7d382e0eb751f5f5cc4b5c5ceac210e4c85737 100644 --- a/src/main/java/net/ivoa/xml/uws/v1/JobSummary.java +++ b/src/main/java/net/ivoa/xml/uws/v1/JobSummary.java @@ -8,6 +8,7 @@ package net.ivoa.xml.uws.v1; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import it.inaf.oats.vospace.datamodel.JobInfoDeserializer; @@ -20,7 +21,6 @@ import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlAnyElement; import javax.xml.bind.annotation.XmlAttribute; import javax.xml.bind.annotation.XmlElement; -import javax.xml.bind.annotation.XmlElementRef; import javax.xml.bind.annotation.XmlElementWrapper; import javax.xml.bind.annotation.XmlRootElement; import javax.xml.bind.annotation.XmlSchemaType; @@ -98,6 +98,7 @@ import org.w3c.dom.Element; // <edit> @XmlSeeAlso({Transfer.class}) // Necessary for setting a Transfer inside the jobInfo property. @XmlRootElement(name = "job") +@JsonIgnoreProperties(ignoreUnknown = true) // </edit> public class JobSummary { diff --git a/src/test/java/it/inaf/oats/vospace/datamodel/NodeUtilsTest.java b/src/test/java/it/inaf/oats/vospace/datamodel/NodeUtilsTest.java index 54c78d3e8c2bf4a238d7635f485581729f4225c7..eb52987bc75d772973804cca85ef6550299d8078 100644 --- a/src/test/java/it/inaf/oats/vospace/datamodel/NodeUtilsTest.java +++ b/src/test/java/it/inaf/oats/vospace/datamodel/NodeUtilsTest.java @@ -1,21 +1,62 @@ -/* - * 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.datamodel; - -import java.util.ArrayList; -import java.util.List; -import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; import org.junit.jupiter.api.Test; - public class NodeUtilsTest { + + @Test + public void testGetPathFromRequestURLString() { + + String requestUrl = "http://localhost/vospace/nodes/a/b/c/"; + assertEquals("/a/b/c", NodeUtils.getPathFromRequestURLString(requestUrl)); + } + + @Test + public void testGetPathWithSpacesFromRequestURLString() { + + String requestUrl = "http://localhost/vospace/nodes/a/b/c%20d%20%C3%A4+%2B.pdf"; + assertEquals("/a/b/c d ä +.pdf", NodeUtils.getPathFromRequestURLString(requestUrl)); + } + + @Test + public void testEncodePathSpecialChars() { + + String specialChars = "ä è#+ /other/+-ò@"; + assertEquals("%C3%A4%20%C3%A8%23%2B%20/other/%2B-%C3%B2%40", NodeUtils.urlEncodePath(specialChars)); + } + + @Test + public void testIllegalBrakets() { + testIllegalChars("<no>.pdf"); + } + + @Test + public void testIllegalQuestionMark() { + testIllegalChars("???.pdf"); + } + + @Test + public void testIllegalQuotes() { + testIllegalChars("\"'.pdf"); + } - + @Test + public void testIllegalSlashEncoded() { + testIllegalChars("%2F.pdf"); + } + + private void testIllegalChars(String illegalString) { + boolean exception = false; + try { + NodeUtils.getPathFromRequestURLString("http://localhost/vospace/nodes/path/to/" + illegalString); + } catch (IllegalArgumentException ex) { + exception = true; + } + assertTrue(exception); + } + //@Test public void getPathFromRequestURLStringTest() { @@ -89,5 +130,4 @@ public class NodeUtilsTest { assertArrayEquals(expected.toArray(), result.toArray()); } - } diff --git a/src/test/java/net/ivoa/xml/uws/v1/JobSummaryTest.java b/src/test/java/net/ivoa/xml/uws/v1/JobSummaryTest.java index 713181de6dd54383f2945e8214d922dad4f5e4ff..6e31e1d37379a1f4262af0d5d67655e9a66f866e 100644 --- a/src/test/java/net/ivoa/xml/uws/v1/JobSummaryTest.java +++ b/src/test/java/net/ivoa/xml/uws/v1/JobSummaryTest.java @@ -51,6 +51,15 @@ public class JobSummaryTest { verifyJobsAreEquals(deserialized); } + + /** + * Uses JSON extracted from real job executed by transfer service. Contains extra field jobType. + */ + @Test + public void testDeserializeTransferServiceResponse() throws Exception { + String response = "{\"jobId\": \"917c784f814c4a1a91a9d5d1af07dbe9\", \"ownerId\": \"2386\", \"jobType\": \"pullToVoSpace\", \"phase\": \"PENDING\", \"startTime\": null, \"endTime\": null, \"creationTime\": \"2021-02-03T15:05:57.233602\", \"jobInfo\": {\"transfer\": {\"view\": null, \"target\": \"vos://example.com!vospace/szorba/aaa\", \"version\": null, \"direction\": \"pullToVoSpace\", \"keepBytes\": null, \"protocols\": [{\"uri\": \"ia2:async-recall\", \"param\": [{\"uri\": \"ia2:node-type\", \"value\": \"single\"}], \"endpoint\": null}]}}, \"results\": null}"; + MAPPER.readValue(response, JobSummary.class); + } private JobSummary getJobSummary() {