Loading src/main/java/it/inaf/oats/vospace/datamodel/NodeProperties.java +13 −22 Original line number Diff line number Diff line /* * 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; Loading @@ -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 Loading @@ -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(); } } Loading @@ -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())) { Loading @@ -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() Loading @@ -97,9 +93,4 @@ public class NodeProperties { } public static String getPropertyURI(String propertyName) { return PROPERTY_BASE_URI.concat(propertyName); } } src/main/java/it/inaf/oats/vospace/datamodel/NodeUtils.java +46 −68 Original line number Diff line number Diff line /* * 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; Loading @@ -11,30 +6,65 @@ 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) { return getPathFromRequestURLString(requestUrlString, "/nodes/"); } String[] split = requestUrlString.split("/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 // But the output has only single slash separators Loading Loading @@ -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); Loading Loading @@ -146,58 +178,4 @@ 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; } } src/main/java/net/ivoa/xml/uws/v1/JobSummary.java +2 −1 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading Loading @@ -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 { Loading src/test/java/it/inaf/oats/vospace/datamodel/NodeUtilsTest.java +52 −12 Original line number Diff line number Diff line /* * 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() { Loading Loading @@ -89,5 +130,4 @@ public class NodeUtilsTest { assertArrayEquals(expected.toArray(), result.toArray()); } } src/test/java/net/ivoa/xml/uws/v1/JobSummaryTest.java +9 −0 Original line number Diff line number Diff line Loading @@ -52,6 +52,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() { JobSummary job = new JobSummary(); Loading Loading
src/main/java/it/inaf/oats/vospace/datamodel/NodeProperties.java +13 −22 Original line number Diff line number Diff line /* * 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; Loading @@ -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 Loading @@ -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(); } } Loading @@ -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())) { Loading @@ -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() Loading @@ -97,9 +93,4 @@ public class NodeProperties { } public static String getPropertyURI(String propertyName) { return PROPERTY_BASE_URI.concat(propertyName); } }
src/main/java/it/inaf/oats/vospace/datamodel/NodeUtils.java +46 −68 Original line number Diff line number Diff line /* * 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; Loading @@ -11,30 +6,65 @@ 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) { return getPathFromRequestURLString(requestUrlString, "/nodes/"); } String[] split = requestUrlString.split("/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 // But the output has only single slash separators Loading Loading @@ -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); Loading Loading @@ -146,58 +178,4 @@ 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; } }
src/main/java/net/ivoa/xml/uws/v1/JobSummary.java +2 −1 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading Loading @@ -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 { Loading
src/test/java/it/inaf/oats/vospace/datamodel/NodeUtilsTest.java +52 −12 Original line number Diff line number Diff line /* * 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() { Loading Loading @@ -89,5 +130,4 @@ public class NodeUtilsTest { assertArrayEquals(expected.toArray(), result.toArray()); } }
src/test/java/net/ivoa/xml/uws/v1/JobSummaryTest.java +9 −0 Original line number Diff line number Diff line Loading @@ -52,6 +52,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() { JobSummary job = new JobSummary(); Loading