diff --git a/src/main/java/it/inaf/oats/vospace/BaseNodeController.java b/src/main/java/it/inaf/oats/vospace/BaseNodeController.java index 81e2a8b6c5eb1021e55cdcd9b76176c57b54be2b..d203e2f8491448a8bc0973031e7dbdad42779100 100644 --- a/src/main/java/it/inaf/oats/vospace/BaseNodeController.java +++ b/src/main/java/it/inaf/oats/vospace/BaseNodeController.java @@ -19,11 +19,11 @@ public abstract class BaseNodeController { @Autowired private HttpServletRequest servletRequest; - + @Value("${vospace-authority}") protected String authority; - - protected String getPath() { + + protected String getPath() { String requestURL = servletRequest.getRequestURL().toString(); try { return NodeUtils.getPathFromRequestURLString(requestURL); @@ -35,7 +35,7 @@ public abstract class BaseNodeController { protected String getParentPath(String path) { return NodeUtils.getParentPath(path); } - + protected void validateAndCheckPayloadURIConsistence(Node node) { // Get Node path (and validates it too) String decodedURIPathFromNode = URIUtils.returnVosPathFromNodeURI(node.getUri(), this.authority); @@ -45,16 +45,27 @@ public abstract class BaseNodeController { if (!decodedURIPathFromNode.equals(this.getPath())) { throw new InvalidURIException(decodedURIPathFromNode, requestPath); } - + } - - protected void validateInternalLinkNode(LinkNode linkNode) { + + protected void validateLinkNode(LinkNode linkNode) { 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); + if (URIUtils.isURIInternal(target)) { + URIUtils.returnVosPathFromNodeURI(linkNode.getTarget(), authority); + } else { + // TODO: Let's discuss if we need to combine this validation with + // protocol endpoints management (URIService, ProtocolType) + // Let's start with http and https only for now + if (!(target.toLowerCase().startsWith("http://") + || target.toLowerCase().startsWith("https://"))) { + throw new InvalidArgumentException("LinkNode target malformed or unsupported protocol: " + target); + } + + } } } diff --git a/src/main/java/it/inaf/oats/vospace/CreateNodeController.java b/src/main/java/it/inaf/oats/vospace/CreateNodeController.java index 4cd2860cf793f73eb7c04a0e4243e3db7f835573..4fa613ff8393b72c1901afa5f9c047ade6291bf4 100644 --- a/src/main/java/it/inaf/oats/vospace/CreateNodeController.java +++ b/src/main/java/it/inaf/oats/vospace/CreateNodeController.java @@ -6,7 +6,6 @@ package it.inaf.oats.vospace; import it.inaf.ia2.aa.data.User; -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; @@ -45,7 +44,7 @@ public class CreateNodeController extends BaseNodeController { private void validateInputNode(Node node) { if (node instanceof LinkNode) { - this.validateInternalLinkNode((LinkNode) node); + this.validateLinkNode((LinkNode) node); } } diff --git a/src/main/java/it/inaf/oats/vospace/FileServiceClient.java b/src/main/java/it/inaf/oats/vospace/FileServiceClient.java index f23a4b293c6496f8802ae2c017af658fa00f3fbe..89d6912f26df618b4b97dcb1049d0fd0adcc6804 100644 --- a/src/main/java/it/inaf/oats/vospace/FileServiceClient.java +++ b/src/main/java/it/inaf/oats/vospace/FileServiceClient.java @@ -7,12 +7,19 @@ package it.inaf.oats.vospace; import com.fasterxml.jackson.databind.ObjectMapper; import it.inaf.ia2.aa.data.User; +import it.inaf.oats.vospace.datamodel.NodeUtils; import it.inaf.oats.vospace.datamodel.Views; import it.inaf.oats.vospace.exception.InvalidArgumentException; +import it.inaf.oats.vospace.exception.NodeNotFoundException; +import it.inaf.oats.vospace.parent.exchange.ArchiveEntryDescriptor; +import it.inaf.oats.vospace.persistence.NodeDAO; import java.io.OutputStream; +import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; import javax.servlet.http.HttpServletRequest; +import net.ivoa.xml.vospace.v2.ContainerNode; +import net.ivoa.xml.vospace.v2.Node; import net.ivoa.xml.vospace.v2.Transfer; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; @@ -21,6 +28,7 @@ import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.stereotype.Component; import org.springframework.web.client.RestTemplate; +import static org.springframework.web.servlet.mvc.method.RequestMappingInfo.paths; @Component public class FileServiceClient { @@ -39,6 +47,12 @@ public class FileServiceClient { @Autowired private HttpServletRequest request; + @Autowired + private LinkService linkService; + + @Autowired + private NodeDAO nodeDAO; + public String startArchiveJob(Transfer transfer, String jobId) { String target = transfer.getTarget().substring("vos://".length() + authority.length()); @@ -64,9 +78,38 @@ public class FileServiceClient { vosPaths.add(target); } + // Generate descriptors + // Expand container nodes into direct children list + List<String> expandedVosPaths = new ArrayList<>(); + + for (String vosPath : vosPaths) { + Node node + = nodeDAO.listNode(vosPath) + .orElseThrow(() -> { + throw new NodeNotFoundException(vosPath); + }); + + if (node instanceof ContainerNode) { + List<Node> nodes = ((ContainerNode) node).getNodes(); + if (nodes.isEmpty()) { + expandedVosPaths.add(NodeUtils.getVosPath(node)); + } else { + expandedVosPaths.addAll(nodes + .stream().map(n -> NodeUtils.getVosPath(n)) + .collect(Collectors.toList())); + } + } else { + expandedVosPaths.add(vosPath); + } + + } + + // follow links to links in vosPaths + List<ArchiveEntryDescriptor> entryDescriptors = linkService.followLinksForArchiveService(expandedVosPaths); + ArchiveRequest archiveRequest = new ArchiveRequest(); archiveRequest.setJobId(jobId); - archiveRequest.setPaths(vosPaths); + archiveRequest.setEntryDescriptors(entryDescriptors); archiveRequest.setType(archiveTypeFromViewUri(transfer.getView().getUri())); String url = fileServiceUrl + "/archive"; @@ -86,17 +129,17 @@ public class FileServiceClient { return res.getHeaders().getLocation().toString(); }, new Object[]{}); } - - public void startFileCopyJob(String sourceVosPath, + + public void startFileCopyJob(String sourceVosPath, String destiantionVosPath, String jobId, User user) { - + CopyRequest copyRequest = new CopyRequest(); copyRequest.setJobId(jobId); copyRequest.setSourceRootVosPath(sourceVosPath); copyRequest.setDestinationRootVosPath(destiantionVosPath); - + String url = fileServiceUrl + "/copy"; - + String token = user.getAccessToken(); restTemplate.execute(url, HttpMethod.POST, req -> { HttpHeaders headers = req.getHeaders(); @@ -108,10 +151,10 @@ public class FileServiceClient { try (OutputStream os = req.getBody()) { MAPPER.writeValue(os, copyRequest); } - }, res -> { - return null; + }, res -> { + return null; }, new Object[]{}); - + } public static class CopyRequest { @@ -150,7 +193,7 @@ public class FileServiceClient { private String type; private String jobId; - private List<String> paths; + private List<ArchiveEntryDescriptor> entryDescriptors; public String getType() { return type; @@ -168,12 +211,12 @@ public class FileServiceClient { this.jobId = jobId; } - public List<String> getPaths() { - return paths; + public List<ArchiveEntryDescriptor> getEntryDescriptors() { + return entryDescriptors; } - public void setPaths(List<String> paths) { - this.paths = paths; + public void setEntryDescriptors(List<ArchiveEntryDescriptor> entryDescriptors) { + this.entryDescriptors = entryDescriptors; } } diff --git a/src/main/java/it/inaf/oats/vospace/LinkService.java b/src/main/java/it/inaf/oats/vospace/LinkService.java new file mode 100644 index 0000000000000000000000000000000000000000..729c643a34a5d83eefc9b5972fbe6b76b9455760 --- /dev/null +++ b/src/main/java/it/inaf/oats/vospace/LinkService.java @@ -0,0 +1,105 @@ +/* + * This file is part of vospace-rest + * Copyright (C) 2021 Istituto Nazionale di Astrofisica + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package it.inaf.oats.vospace; + +import it.inaf.oats.vospace.datamodel.NodeUtils; +import it.inaf.oats.vospace.exception.InternalFaultException; +import it.inaf.oats.vospace.parent.exchange.ArchiveEntryDescriptor; +import it.inaf.oats.vospace.persistence.NodeDAO; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import net.ivoa.xml.vospace.v2.LinkNode; +import net.ivoa.xml.vospace.v2.Node; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +@Service +public class LinkService { + + @Value("${vospace-authority}") + private String authority; + + @Value("${link-max-depth}") + private int linkMaxDepth; + + @Autowired + private NodeDAO nodeDao; + + // Returns a new list = the list in argument with paths of links to links are substituted with + // their actual destination vos path. Only links to external resources + // (http:// ... ) should remain + public List<ArchiveEntryDescriptor> followLinksForArchiveService(List<String> vosPaths) { + + List<LinkNode> linkNodesInPaths = nodeDao.returnLinkNodesInList(vosPaths); + + // No links no change + if(linkNodesInPaths.isEmpty()) + return vosPaths.stream().map(p -> new ArchiveEntryDescriptor(p)) + .collect(Collectors.toList()); + + List<String> linkVosPaths = linkNodesInPaths.stream() + .map(ln -> NodeUtils.getVosPath(ln)).collect(Collectors.toList()); + + // Safe copy of argument + List<String> resultVosPaths = new ArrayList<>(vosPaths); + + resultVosPaths.removeAll(linkVosPaths); + + // Generate descriptors from non link vospaths + List<ArchiveEntryDescriptor> resultDescriptors = + resultVosPaths.stream().map(p -> new ArchiveEntryDescriptor(p)) + .collect(Collectors.toList()); + + // Add descriptors from links + resultDescriptors.addAll( + linkNodesInPaths.stream().map(p -> getLinkNodeArchiveEntryDescriptor(p)) + .collect(Collectors.toList()) + ); + + return resultDescriptors; + + } + + private ArchiveEntryDescriptor getLinkNodeArchiveEntryDescriptor(LinkNode node){ + String vosPath = NodeUtils.getVosPath(node); + String targetNodeVosPath = NodeUtils.getVosPath(this.followLink(node)); + + return new ArchiveEntryDescriptor(vosPath, targetNodeVosPath); + } + + public Node followLink(LinkNode linkNode) { + return this.followLinkRecursive(linkNode, 0); + } + + private Node followLinkRecursive(LinkNode linkNode, int depth) { + + if (depth >= linkMaxDepth) { + throw new InternalFaultException("Max link depth reached at link node: " + + NodeUtils.getVosPath(linkNode)); + } + + String linkTarget = linkNode.getTarget(); + + if (URIUtils.isURIInternal(linkTarget)) { + String targetPath = URIUtils.returnVosPathFromNodeURI(linkTarget, authority); + + Optional<Node> targetNodeOpt = nodeDao.listNode(targetPath); + Node targetNode = targetNodeOpt.orElseThrow(() -> new InternalFaultException("Broken Link to target: " + targetPath)); + + if (targetNode instanceof LinkNode) { + return this.followLinkRecursive(linkNode, ++depth); + } else { + return targetNode; + } + } else { + return linkNode; + } + } + +} diff --git a/src/main/java/it/inaf/oats/vospace/SetNodeController.java b/src/main/java/it/inaf/oats/vospace/SetNodeController.java index 7659ab947f53823a356ff7a46328236d190cdcd2..aa712ecb4f8737778f4067b12e0be824e3d93c55 100644 --- a/src/main/java/it/inaf/oats/vospace/SetNodeController.java +++ b/src/main/java/it/inaf/oats/vospace/SetNodeController.java @@ -73,7 +73,7 @@ public class SetNodeController extends BaseNodeController { if (node instanceof DataNode) { checkViews((DataNode) node, (DataNode) toBeModifiedNode); } else if(node instanceof LinkNode) { - this.validateInternalLinkNode((LinkNode) node); + this.validateLinkNode((LinkNode) node); } //The service SHOULD throw a HTTP 500 status code including an InternalFault fault diff --git a/src/main/java/it/inaf/oats/vospace/URIUtils.java b/src/main/java/it/inaf/oats/vospace/URIUtils.java index 0250050fd271a772725b8a1097f4a63a367c1f94..36bd239aff98a538cf34d445f402d049577c0465 100644 --- a/src/main/java/it/inaf/oats/vospace/URIUtils.java +++ b/src/main/java/it/inaf/oats/vospace/URIUtils.java @@ -18,6 +18,13 @@ public class URIUtils { // Slashes are treated separately private static final Pattern FORBIDDEN_CHARS = Pattern.compile("[\\x00\\x08\\x0B\\x0C\\x0E-\\x1F" + Pattern.quote("<>?\":\\|'`*") + "]"); private static final String SCHEME = "vos"; + + public static boolean isURIInternal(String URI) { + if(URI == null) + throw new IllegalArgumentException("URI can't be null"); + + return URI.toLowerCase().startsWith(SCHEME); + } public static String returnURIFromVosPath(String vosPath, String authority) { String result = null; diff --git a/src/main/java/it/inaf/oats/vospace/UriService.java b/src/main/java/it/inaf/oats/vospace/UriService.java index 056aa04248b782adcbdd0576ecb876e8d7256b46..aacc7fa2c8bc477b7c50b7cfd3a8fab2797b9081 100644 --- a/src/main/java/it/inaf/oats/vospace/UriService.java +++ b/src/main/java/it/inaf/oats/vospace/UriService.java @@ -19,6 +19,7 @@ import it.inaf.oats.vospace.exception.NodeNotFoundException; import it.inaf.oats.vospace.exception.PermissionDeniedException; import it.inaf.oats.vospace.exception.ProtocolNotSupportedException; import it.inaf.oats.vospace.exception.NodeBusyException; +import it.inaf.oats.vospace.parent.persistence.LinkedServiceDAO; import it.inaf.oats.vospace.persistence.LocationDAO; import it.inaf.oats.vospace.persistence.NodeDAO; import it.inaf.oats.vospace.persistence.model.Location; @@ -27,6 +28,7 @@ import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.stream.Collectors; import javax.servlet.http.HttpServletRequest; @@ -49,15 +51,18 @@ public class UriService { @Value("${file-service-url}") private String fileServiceUrl; - @Value("${link-max-depth}") - private int linkMaxDepth; - @Autowired private NodeDAO nodeDao; + + @Autowired + private LinkService linkService; @Autowired private LocationDAO locationDAO; + @Autowired + private LinkedServiceDAO linkedServiceDAO; + @Autowired private HttpServletRequest servletRequest; @@ -90,14 +95,15 @@ public class UriService { JobService.JobDirection jobDirection = JobDirection.getJobDirectionEnumFromTransfer(transfer); - List<String> validProtocolUris = new ArrayList<>(); + List<ProtocolType> validProtocolTypes = new ArrayList<>(); switch (jobDirection) { case pullFromVoSpace: + validProtocolTypes.add(ProtocolType.HTTPSGET); case pullToVoSpace: - validProtocolUris.add("ivo://ivoa.net/vospace/core#httpget"); + validProtocolTypes.add(ProtocolType.HTTPGET); break; case pushToVoSpace: - validProtocolUris.add("ivo://ivoa.net/vospace/core#httpput"); + validProtocolTypes.add(ProtocolType.HTTPPUT); break; default: @@ -106,15 +112,30 @@ public class UriService { List<Protocol> validProtocols = transfer.getProtocols().stream() - // discard invalid protocols - .filter(protocol -> validProtocolUris.contains(protocol.getUri())) + // discard invalid protocols by uri String + .filter(protocol + -> validProtocolTypes.stream().map(pt + -> { + return pt.getUri(); + }) + .collect(Collectors.toList()) + .contains(protocol.getUri())) .map(p -> { // set endpoints - Protocol protocol = new Protocol(); - protocol.setUri(p.getUri()); - protocol.setEndpoint(getEndpoint(job, transfer)); - return protocol; - }).collect(Collectors.toList()); + String endpoint = getEndpoint(job, transfer); + ProtocolType pt + = ProtocolType.getProtocolTypeFromURI(p.getUri()); + + if (pt.isEndpointCompliant(endpoint)) { + Protocol protocol = new Protocol(); + protocol.setUri(p.getUri()); + protocol.setEndpoint(endpoint); + return protocol; + } else { + return null; + } + }).filter(Objects::nonNull) + .collect(Collectors.toList()); if (validProtocols.isEmpty()) { Protocol protocol = transfer.getProtocols().get(0); @@ -137,7 +158,7 @@ public class UriService { if (!NodeUtils.checkIfReadable(node, user.getName(), user.getGroups())) { throw PermissionDeniedException.forPath(relativePath); } - node = this.followLink((LinkNode) node); + node = linkService.followLink((LinkNode) node); } } return node; @@ -168,7 +189,7 @@ public class UriService { JobService.JobDirection jobType = JobDirection.getJobDirectionEnumFromTransfer(transfer); Node node = this.getEndpointNode(relativePath, jobType, user); - + switch (jobType) { case pushToVoSpace: case pullToVoSpace: @@ -178,7 +199,7 @@ public class UriService { break; case pullFromVoSpace: - // Refresh relative path: it can differ in case of links + // Refresh relative path: it can differ in case of links followed relativePath = NodeUtils.getVosPath(node); if (!NodeUtils.checkIfReadable(node, creator, groups)) { throw PermissionDeniedException.forPath(relativePath); @@ -197,25 +218,36 @@ public class UriService { return fileServiceClient.startArchiveJob(transfer, job.getJobId()); } - Location location = locationDAO.getNodeLocation(relativePath).orElse(null); + boolean isLinkNode = node instanceof LinkNode; String endpoint; - if (location != null && location.getType() == LocationType.PORTAL) { - String fileName = nodeDao.getNodeOsName(relativePath); - endpoint = "http://" + location.getSource().getHostname() + location.getSource().getBaseUrl(); - if (!endpoint.endsWith("/")) { - endpoint += "/"; + if (isLinkNode) { + endpoint = ((LinkNode) node).getTarget(); + String linkTarget = ((LinkNode) node).getTarget(); + if (linkedServiceDAO.isLinkedServiceUrl(linkTarget)) { + endpoint += "?token=" + getEndpointToken(linkTarget); } - endpoint += fileName; } else { - endpoint = fileServiceUrl + urlEncodePath(relativePath); - } - endpoint += "?jobId=" + job.getJobId(); + Location location = locationDAO.getNodeLocation(relativePath).orElse(null); - if (!"true".equals(NodeProperties.getNodePropertyByURI(node, NodeProperties.PUBLIC_READ_URI))) { - endpoint += "&token=" + getEndpointToken(fileServiceUrl + relativePath); + if (location != null && location.getType() == LocationType.PORTAL) { + String fileName = nodeDao.getNodeOsName(relativePath); + endpoint = "http://" + location.getSource().getHostname() + location.getSource().getBaseUrl(); + if (!endpoint.endsWith("/")) { + endpoint += "/"; + } + endpoint += fileName; + } else { + endpoint = fileServiceUrl + urlEncodePath(relativePath); + } + + endpoint += "?jobId=" + job.getJobId(); + + if (!"true".equals(NodeProperties.getNodePropertyByURI(node, NodeProperties.PUBLIC_READ_URI))) { + endpoint += "&token=" + getEndpointToken(fileServiceUrl + relativePath); + } } return endpoint; @@ -282,26 +314,40 @@ public class UriService { return (Transfer) job.getJobInfo().getAny().get(0); } - private Node followLink(LinkNode linkNode) { - return this.followLinkRecursive(linkNode, 0); - } + public enum ProtocolType { + // Please keep the URIs in this enum UNIQUE! + // added a unit test to check this + HTTPGET("ivo://ivoa.net/vospace/core#httpget", "http"), + HTTPSGET("ivo://ivoa.net/vospace/core#httpsget", "https"), + HTTPPUT("ivo://ivoa.net/vospace/core#httpput", "http"), + HTTPSPUT("ivo://ivoa.net/vospace/core#httpsput", "https"); + + private final String uri; + private final String protocolString; + + private ProtocolType(String uri, String protocolString) { + this.uri = uri; + this.protocolString = protocolString; + } - private Node followLinkRecursive(LinkNode linkNode, int depth) { - - if(depth >= linkMaxDepth) { - throw new InternalFaultException("Max link depth reached at link node: " - + NodeUtils.getVosPath(linkNode)); + public String getUri() { + return this.uri; } - - String targetPath = URIUtils.returnVosPathFromNodeURI(linkNode.getTarget(), authority); - - Optional<Node> targetNodeOpt = nodeDao.listNode(targetPath); - Node targetNode = targetNodeOpt.orElseThrow(() -> new InternalFaultException("Broken Link to target: " + targetPath)); - - if(targetNode instanceof LinkNode) { - return this.followLinkRecursive(linkNode, ++depth); - } else { - return targetNode; + + public boolean isEndpointCompliant(String endpoint) { + return endpoint.toLowerCase() + .startsWith(this.protocolString + "://"); } + + public static ProtocolType getProtocolTypeFromURI(String uri) { + for (ProtocolType pt : ProtocolType.values()) { + if (pt.getUri().equals(uri)) { + return pt; + } + } + + return null; + } + } } diff --git a/src/main/java/it/inaf/oats/vospace/persistence/NodeDAO.java b/src/main/java/it/inaf/oats/vospace/persistence/NodeDAO.java index ed25fc38d4124abc84ec3d2b5a2afd1f9ba1cf38..e1eac2941bf912ac23cc41aac8b07888e121fa37 100644 --- a/src/main/java/it/inaf/oats/vospace/persistence/NodeDAO.java +++ b/src/main/java/it/inaf/oats/vospace/persistence/NodeDAO.java @@ -18,6 +18,7 @@ import java.sql.SQLException; import java.sql.Types; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -174,7 +175,7 @@ public class NodeDAO { ps.setArray(++i, fromPropertyToArray(ps, NodeProperties.getNodePropertyByURI(newNode, NodeProperties.GROUP_WRITE_URI))); ps.setBoolean(++i, Boolean.valueOf(NodeProperties.getNodePropertyByURI(newNode, NodeProperties.PUBLIC_READ_URI))); if (isLinkNode) { - ps.setString(++i, ((LinkNode) newNode).getTarget() ); + ps.setString(++i, ((LinkNode) newNode).getTarget()); } ps.setString(++i, vosPath); return ps; @@ -552,6 +553,36 @@ public class NodeDAO { } } + public List<LinkNode> returnLinkNodesInList(List<String> vosPaths) { + + if (vosPaths.isEmpty()) { + throw new IllegalArgumentException("Received empty list of paths"); + } + + String sql = "SELECT n.node_id, get_vos_path(n.node_id) as vos_path, n.name,\n" + + "n.type, n.async_trans, n.sticky, n.job_id IS NOT NULL AS busy_state, n.creator_id, n.group_read, n.group_write,\n" + + "n.is_public, n.content_length, n.created_on, n.last_modified, n.accept_views, n.provide_views, n.quota, n.content_md5, n.target\n" + + "FROM node n\n" + + "WHERE (" + String.join(" OR ", Collections.nCopies(vosPaths.size(), "n.node_id = id_from_vos_path(?)")) + ")\n" + + "AND n.type = 'link'\n"; + + return jdbcTemplate.query(conn -> { + PreparedStatement ps = conn.prepareStatement(sql); + int i = 0; + for (String vosPath : vosPaths) { + ps.setString(++i, vosPath); + } + return ps; + }, rs -> { + List<LinkNode> linkNodes = new ArrayList<>(); + while (rs.next()) { + linkNodes.add((LinkNode) this.getNodeFromResultSet(rs)); + } + return linkNodes; + }); + + } + private String getGroupsString(ResultSet rs, String column) throws SQLException { Array array = rs.getArray(column); if (array == null) { diff --git a/src/test/java/it/inaf/oats/vospace/CreateNodeControllerTest.java b/src/test/java/it/inaf/oats/vospace/CreateNodeControllerTest.java index f9dcff79ee7bb9fbbc6ae1928e4ea1ad95796a11..09a78a2e700adce65981c827e67d3ef815db5f55 100644 --- a/src/test/java/it/inaf/oats/vospace/CreateNodeControllerTest.java +++ b/src/test/java/it/inaf/oats/vospace/CreateNodeControllerTest.java @@ -242,6 +242,25 @@ public class CreateNodeControllerTest { verifyLinkArguments(null); } + + @Test + public void testCreateLinkNodeExternalHttp() throws Exception { + String requestBody = getResourceFileContent("create-link-node-external-http.xml"); + + when(nodeDao.listNode(eq("/"))) + .thenReturn(Optional.of(getContainerParentNode("/"))); + + mockMvc.perform(put("/nodes/myExternalHttpLink") + .header("Authorization", "Bearer user2_token") + .content(requestBody) + .contentType(MediaType.APPLICATION_XML) + .accept(MediaType.APPLICATION_XML)) + .andDo(print()) + .andExpect(status().isOk()); + + verifyLinkArguments("http://www.external.com/files/file.txt"); + + } @Test public void testNodeAlreadyExisting() throws Exception { diff --git a/src/test/java/it/inaf/oats/vospace/FileServiceClientTest.java b/src/test/java/it/inaf/oats/vospace/FileServiceClientTest.java index b57b7d19a533e08049b6e2581a02f3a33d3480af..9ec78b9fb679cb4250975eb692873f656978d8e2 100644 --- a/src/test/java/it/inaf/oats/vospace/FileServiceClientTest.java +++ b/src/test/java/it/inaf/oats/vospace/FileServiceClientTest.java @@ -10,11 +10,17 @@ import it.inaf.ia2.aa.data.User; import it.inaf.oats.vospace.FileServiceClient.ArchiveRequest; import it.inaf.oats.vospace.datamodel.Views; import it.inaf.oats.vospace.exception.InvalidArgumentException; +import it.inaf.oats.vospace.parent.exchange.ArchiveEntryDescriptor; +import it.inaf.oats.vospace.persistence.NodeDAO; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.UncheckedIOException; import java.net.URI; +import java.util.List; +import java.util.Optional; import javax.servlet.http.HttpServletRequest; +import net.ivoa.xml.vospace.v2.ContainerNode; +import net.ivoa.xml.vospace.v2.DataNode; import net.ivoa.xml.vospace.v2.Param; import net.ivoa.xml.vospace.v2.Transfer; import net.ivoa.xml.vospace.v2.View; @@ -28,6 +34,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import org.mockito.InjectMocks; import org.mockito.Mock; +import org.mockito.Mockito; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -54,6 +61,12 @@ public class FileServiceClientTest { @Mock private HttpServletRequest request; + + @Mock + private LinkService linkService; + + @Mock + private NodeDAO nodeDAO; @InjectMocks private FileServiceClient fileServiceClient; @@ -63,7 +76,7 @@ public class FileServiceClientTest { ReflectionTestUtils.setField(fileServiceClient, "authority", "example.com!vospace"); ReflectionTestUtils.setField(fileServiceClient, "fileServiceUrl", "http://file-service"); } - + @Test public void testTarArchiveJob() { testStartArchiveJob(Views.TAR_VIEW_URI); @@ -92,11 +105,19 @@ public class FileServiceClientTest { View view = new View(); view.setUri(Views.ZIP_VIEW_URI); transfer.setView(view); + + ContainerNode node = Mockito.mock(ContainerNode.class); + when(node.getNodes()).thenReturn(List.of()); + when(node.getUri()).thenReturn("vos://example.com!vospace/mydir"); + when(nodeDAO.listNode(eq("/mydir"))).thenReturn(Optional.of(node)); + + when(linkService.followLinksForArchiveService(any())) + .thenReturn(List.of(new ArchiveEntryDescriptor("/mydir"))); ArchiveRequest archiveRequest = testStartArchiveJob(transfer); - assertEquals(1, archiveRequest.getPaths().size()); - assertEquals("/mydir", archiveRequest.getPaths().get(0)); + assertEquals(1, archiveRequest.getEntryDescriptors().size()); + assertEquals("/mydir", archiveRequest.getEntryDescriptors().get(0).getTargetNodeVosPath()); } @Test @@ -153,12 +174,27 @@ public class FileServiceClientTest { param2.setUri(viewUri + "/include"); param2.setValue("file2"); view.getParam().add(param2); + + DataNode node1 = Mockito.mock(DataNode.class); + when(node1.getUri()).thenReturn("vos://example.com!vospace/parent_dir/file1"); + when(nodeDAO.listNode(eq("/parent_dir/file1"))).thenReturn(Optional.of(node1)); + + DataNode node2 = Mockito.mock(DataNode.class); + when(node2.getUri()).thenReturn("vos://example.com!vospace/parent_dir/file2"); + when(nodeDAO.listNode(eq("/parent_dir/file2"))).thenReturn(Optional.of(node2)); + + when(linkService.followLinksForArchiveService(any())).thenReturn( + List.of(new ArchiveEntryDescriptor("/parent_dir/file1"), + new ArchiveEntryDescriptor("/parent_dir/file2") + )); ArchiveRequest archiveRequest = testStartArchiveJob(transfer); - assertEquals(2, archiveRequest.getPaths().size()); - assertEquals("/parent_dir/file1", archiveRequest.getPaths().get(0)); - assertEquals("/parent_dir/file2", archiveRequest.getPaths().get(1)); + assertEquals(2, archiveRequest.getEntryDescriptors().size()); + assertEquals("/parent_dir/file1", + archiveRequest.getEntryDescriptors().get(0).getTargetNodeVosPath()); + assertEquals("/parent_dir/file2", + archiveRequest.getEntryDescriptors().get(1).getTargetNodeVosPath()); } private ArchiveRequest testStartArchiveJob(Transfer transfer) { diff --git a/src/test/java/it/inaf/oats/vospace/LinkServiceTest.java b/src/test/java/it/inaf/oats/vospace/LinkServiceTest.java new file mode 100644 index 0000000000000000000000000000000000000000..091e4d0ccd7348c8c494a3b6ec0e0646134025f6 --- /dev/null +++ b/src/test/java/it/inaf/oats/vospace/LinkServiceTest.java @@ -0,0 +1,61 @@ +/* + * This file is part of vospace-rest + * Copyright (C) 2021 Istituto Nazionale di Astrofisica + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package it.inaf.oats.vospace; + +import it.inaf.ia2.aa.data.User; +import it.inaf.oats.vospace.exception.NodeBusyException; +import it.inaf.oats.vospace.exception.NodeNotFoundException; +import it.inaf.oats.vospace.exception.PermissionDeniedException; +import it.inaf.oats.vospace.parent.exchange.ArchiveEntryDescriptor; +import it.inaf.oats.vospace.persistence.DataSourceConfigSingleton; +import it.inaf.oats.vospace.persistence.NodeDAO; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import net.ivoa.xml.vospace.v2.Transfer; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.ContextConfiguration; + +@SpringBootTest +//@AutoConfigureMockMvc +@TestPropertySource(locations = "classpath:test.properties", properties = {"vospace-authority=example.com!vospace", "file-service-url=http://file-service"}) +@ContextConfiguration(classes = {DataSourceConfigSingleton.class}) +public class LinkServiceTest { + + @Autowired + private LinkService linkService; + + @Test + public void testFollowLinksForArchiveService() { + List<String> vosPaths = List.of("/test3/m1/link2", "/test3/m1/m2"); + + List<ArchiveEntryDescriptor> aed = linkService.followLinksForArchiveService(vosPaths); + + assertEquals(2, aed.size()); + List<String> targetVosPaths = aed.stream().map(a -> a.getTargetNodeVosPath()) + .collect(Collectors.toList()); + + System.out.println(targetVosPaths); + assertTrue(targetVosPaths.containsAll(List.of("/test3/m1/m2", "/test1/f1/f2_renamed/f3"))); + + } + + +} diff --git a/src/test/java/it/inaf/oats/vospace/URIUtilsTest.java b/src/test/java/it/inaf/oats/vospace/URIUtilsTest.java index d630f85de6e0161d6216227d8613799dc3b80beb..6897ae542576e46865db1b494bbb116433f54849 100644 --- a/src/test/java/it/inaf/oats/vospace/URIUtilsTest.java +++ b/src/test/java/it/inaf/oats/vospace/URIUtilsTest.java @@ -8,7 +8,9 @@ package it.inaf.oats.vospace; import it.inaf.oats.vospace.exception.InvalidURIException; import java.net.URISyntaxException; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import org.junit.jupiter.api.Test; public class URIUtilsTest { @@ -84,6 +86,12 @@ public class URIUtilsTest { String test3 = URIUtils.returnURIFromVosPath("/test1/te# !?st2", authority); assertEquals("vos://"+authority+"/test1/te%23%20!%3Fst2", test3); } + + @Test + public void testIsURIInternal() { + assertTrue(URIUtils.isURIInternal("vos://"+authority+"/test1/test2")); + assertFalse(URIUtils.isURIInternal("http://host.ia2.inaf.it/test1/test2")); + } } diff --git a/src/test/java/it/inaf/oats/vospace/UriServiceTest.java b/src/test/java/it/inaf/oats/vospace/UriServiceTest.java index b14dc36197da4be78525ce9f350dbc2fc8802d9a..3b98d4fa219678f64c6cff19d9e9cf8527396c5b 100644 --- a/src/test/java/it/inaf/oats/vospace/UriServiceTest.java +++ b/src/test/java/it/inaf/oats/vospace/UriServiceTest.java @@ -7,6 +7,7 @@ package it.inaf.oats.vospace; import it.inaf.ia2.aa.ServletRapClient; import it.inaf.ia2.aa.data.User; +import it.inaf.oats.vospace.UriService.ProtocolType; import it.inaf.oats.vospace.datamodel.NodeProperties; import it.inaf.oats.vospace.datamodel.NodeUtils; import it.inaf.oats.vospace.datamodel.Views; @@ -15,11 +16,14 @@ import it.inaf.oats.vospace.exception.InvalidArgumentException; import it.inaf.oats.vospace.exception.NodeBusyException; import it.inaf.oats.vospace.exception.PermissionDeniedException; import it.inaf.oats.vospace.exception.ProtocolNotSupportedException; +import it.inaf.oats.vospace.parent.persistence.LinkedServiceDAO; import it.inaf.oats.vospace.persistence.LocationDAO; import it.inaf.oats.vospace.persistence.NodeDAO; import it.inaf.oats.vospace.persistence.model.Location; import it.inaf.oats.vospace.persistence.model.LocationType; +import java.util.List; import java.util.Optional; +import java.util.stream.Collectors; import javax.servlet.http.HttpServletRequest; import net.ivoa.xml.uws.v1.JobSummary; import net.ivoa.xml.vospace.v2.ContainerNode; @@ -69,6 +73,9 @@ public class UriServiceTest { @MockBean private CreateNodeService createNodeService; + + @MockBean + private LinkedServiceDAO linkedServiceDAO; @MockBean private FileServiceClient fileServiceClient; @@ -94,12 +101,15 @@ public class UriServiceTest { return request; } } - + + public final static String PORTAL_URL = "http://portalurl.ia2.inaf.it/portal/files"; + @BeforeEach public void init() { Location location = new Location(); location.setType(LocationType.ASYNC); when(locationDAO.getNodeLocation(any())).thenReturn(Optional.of(location)); + when(linkedServiceDAO.isLinkedServiceUrl(eq(PORTAL_URL))).thenReturn(Boolean.TRUE); } @Test @@ -381,14 +391,7 @@ public class UriServiceTest { when(user.getName()).thenReturn("user1"); when(servletRequest.getUserPrincipal()).thenReturn(user); - - - when(rapClient.exchangeToken(argThat(req -> { - assertEquals("<token>", req.getSubjectToken()); - assertEquals("http://file-service/mydummydata1", req.getResource()); - return true; - }), any())).thenReturn("<new-token>"); - + JobSummary job = getPullFromVoSpaceJob(targetOfPull); Transfer tr = uriService.getTransfer(job); @@ -589,9 +592,13 @@ public class UriServiceTest { mockPublicNode("parent_dir"); mockPublicNode("parent_dir/file1"); mockPublicNode("parent_dir/file2"); + + when(fileServiceClient.startArchiveJob(any(), any())) + .thenReturn("http://file-service/mockarchive"); uriService.getNegotiatedTransfer(job, transfer); - + + verify(fileServiceClient, times(1)).startArchiveJob(transfer, "archive-job-id"); } @@ -667,4 +674,14 @@ public class UriServiceTest { return job; } + + @Test + public void testProtocolType() { + List<ProtocolType> pts = List.of(ProtocolType.values()); + + int distinctUris = + pts.stream().map(pt -> pt.getUri()).distinct().collect(Collectors.toList()).size(); + + assertEquals(pts.size(), distinctUris); + } } diff --git a/src/test/resources/create-link-node-external-http.xml b/src/test/resources/create-link-node-external-http.xml new file mode 100644 index 0000000000000000000000000000000000000000..8046202a52021bb4eaa4b233615dfc25138e36e9 --- /dev/null +++ b/src/test/resources/create-link-node-external-http.xml @@ -0,0 +1,11 @@ +<vos:node xmlns:xs="http://www.w3.org/2001/XMLSchema-instance" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:vos="http://www.ivoa.net/xml/VOSpace/v2.0" xsi:type="vos:LinkNode" uri="vos://example.com!vospace/myExternalHttpLink"> + <vos:properties> + <vos:property uri="ivo://ivoa.net/vospace/core#description">test value</vos:property> + </vos:properties> + <vos:target>http://www.external.com/files/file.txt</vos:target> + <vos:accepts/> + <vos:provides/> + <vos:capabilities/> +</vos:node> \ No newline at end of file diff --git a/src/test/resources/test-data.sql b/src/test/resources/test-data.sql index 3ae4a612a559bf2d4cdfa798e32f386c98f6a0a1..b9e8f6d18bb59a785e9e6dd52c923bfda64f70e2 100644 --- a/src/test/resources/test-data.sql +++ b/src/test/resources/test-data.sql @@ -1,3 +1,5 @@ +INSERT INTO linked_service(service_base_url) VALUES ('http://archives.ia2.inaf.it/files/aao'); + INSERT INTO storage (storage_type, base_path, base_url, hostname) VALUES ('cold', '/ia2_tape/users', NULL, 'tape-server'); INSERT INTO storage (storage_type, base_path, base_url, hostname) VALUES ('hot', '/mnt/hot_storage/users', NULL, 'server'); INSERT INTO storage (storage_type, base_path, base_url, hostname) VALUES ('local', '/home', NULL, 'localhost'); @@ -41,6 +43,7 @@ INSERT INTO node (parent_path, parent_relative_path, name, type, creator_id, gro INSERT INTO node (parent_path, parent_relative_path, name, type, creator_id, group_write, group_read, is_public, location_id) VALUES ('19', '', 'destination2', 'container', 'user3', '{"group1"}', '{"group1"}', false, 3); -- /mycontainer/destination2 INSERT INTO node (parent_path, parent_relative_path, name, type, creator_id, group_write, group_read, is_public, location_id) VALUES ('19.21', '20', 'control', 'container', 'user3', '{"group1"}', '{"group1"}', false, 3); -- /mycontainer/destination2/control +INSERT INTO node (parent_path, parent_relative_path, name, type, creator_id, group_write, group_read, is_public, location_id, target) VALUES ('9.10', '', 'link2', 'link', 'user3', '{"group1"}', '{"group1"}', false, 3, 'vos://example.com!vospace/test1/f1/f2_renamed/f3'); -- /test3/m1/link2 DELETE FROM job;