diff --git a/src/main/java/it/inaf/oats/vospace/UriService.java b/src/main/java/it/inaf/oats/vospace/UriService.java index 0f23ddbbeb80ec0b6335ffc736125df6ccb13622..056aa04248b782adcbdd0576ecb876e8d7256b46 100644 --- a/src/main/java/it/inaf/oats/vospace/UriService.java +++ b/src/main/java/it/inaf/oats/vospace/UriService.java @@ -49,6 +49,9 @@ public class UriService { @Value("${file-service-url}") private String fileServiceUrl; + @Value("${link-max-depth}") + private int linkMaxDepth; + @Autowired private NodeDAO nodeDao; @@ -165,7 +168,7 @@ public class UriService { JobService.JobDirection jobType = JobDirection.getJobDirectionEnumFromTransfer(transfer); Node node = this.getEndpointNode(relativePath, jobType, user); - + switch (jobType) { case pushToVoSpace: case pullToVoSpace: @@ -175,6 +178,8 @@ public class UriService { break; case pullFromVoSpace: + // Refresh relative path: it can differ in case of links + relativePath = NodeUtils.getVosPath(node); if (!NodeUtils.checkIfReadable(node, creator, groups)) { throw PermissionDeniedException.forPath(relativePath); } @@ -278,9 +283,25 @@ public class UriService { } private Node followLink(LinkNode linkNode) { - String targetPath = URIUtils.returnVosPathFromNodeURI(linkNode.getTarget(), authority); - Optional<Node> targetNode = nodeDao.listNode(targetPath); - return targetNode.orElseThrow(() -> new InternalFaultException("Broken Link to target: " + targetPath)); + 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 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; + } } } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 924e80d7a965fadee745d05137191ac73b2c1f23..bdc465f9f511ca3fabea5d15cdeca7c533796c1b 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -18,4 +18,7 @@ logging.level.org.springframework.web=TRACE vospace-authority=example.com!vospace +#tune max depth for chained links +link-max-depth=10 + file-service-url=http://localhost:8087 diff --git a/src/test/java/it/inaf/oats/vospace/TransferControllerTest.java b/src/test/java/it/inaf/oats/vospace/TransferControllerTest.java index 2a523876e00ffd6e1c23d0bdbf5325c985d2d6e9..81a40c085f9fa7493aa55cc50da13d70e55d80d1 100644 --- a/src/test/java/it/inaf/oats/vospace/TransferControllerTest.java +++ b/src/test/java/it/inaf/oats/vospace/TransferControllerTest.java @@ -114,7 +114,7 @@ public class TransferControllerTest { @Test public void testPullFromVoSpaceSync() throws Exception { - Node node = mockPublicDataNode(); + Node node = mockPublicDataNode("vos://example.com!vospace/mynode"); when(nodeDao.listNode(eq("/mynode"))).thenReturn(Optional.of(node)); String requestBody = getResourceFileContent("pullFromVoSpace.xml"); @@ -198,7 +198,7 @@ public class TransferControllerTest { private void testVoSpaceAsyncTransfer(String path, String requestBody) throws Exception { - Node node = mockPublicDataNode(); + Node node = mockPublicDataNode(path); when(nodeDao.listNode(eq(path))).thenReturn(Optional.of(node)); String redirect = mockMvc.perform(post("/transfers?PHASE=RUN") @@ -216,7 +216,7 @@ public class TransferControllerTest { @Test public void testSetJobPhase() throws Exception { - Node node = mockPublicDataNode(); + Node node = mockPublicDataNode("vos://example.com!vospace/mynode"); when(nodeDao.listNode(eq("/mynode"))).thenReturn(Optional.of(node)); JobSummary job = getFakePendingJob(); @@ -268,8 +268,9 @@ public class TransferControllerTest { assertEquals("PENDING", phase); } - private Node mockPublicDataNode() { + private Node mockPublicDataNode(String nodeURI) { Node node = new DataNode(); + node.setUri(nodeURI); Property property = new Property(); property.setUri("ivo://ivoa.net/vospace/core#publicread"); property.setValue("true"); @@ -415,7 +416,7 @@ public class TransferControllerTest { @Test public void testSyncTransferUrlParamsMode() throws Exception { - Node node = mockPublicDataNode(); + Node node = mockPublicDataNode("vos://example.com!vospace/mynode"); when(nodeDao.listNode(eq("/mynode"))).thenReturn(Optional.of(node)); mockMvc.perform(get("/synctrans") diff --git a/src/test/java/it/inaf/oats/vospace/UriServiceTest.java b/src/test/java/it/inaf/oats/vospace/UriServiceTest.java index c0a911c51012b4cf9ec43ccfc40c0e63969118a1..b14dc36197da4be78525ce9f350dbc2fc8802d9a 100644 --- a/src/test/java/it/inaf/oats/vospace/UriServiceTest.java +++ b/src/test/java/it/inaf/oats/vospace/UriServiceTest.java @@ -8,7 +8,9 @@ package it.inaf.oats.vospace; import it.inaf.ia2.aa.ServletRapClient; 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.datamodel.Views; +import it.inaf.oats.vospace.exception.InternalFaultException; import it.inaf.oats.vospace.exception.InvalidArgumentException; import it.inaf.oats.vospace.exception.NodeBusyException; import it.inaf.oats.vospace.exception.PermissionDeniedException; @@ -22,6 +24,7 @@ import javax.servlet.http.HttpServletRequest; import net.ivoa.xml.uws.v1.JobSummary; import net.ivoa.xml.vospace.v2.ContainerNode; import net.ivoa.xml.vospace.v2.DataNode; +import net.ivoa.xml.vospace.v2.LinkNode; import net.ivoa.xml.vospace.v2.Node; import net.ivoa.xml.vospace.v2.Param; import net.ivoa.xml.vospace.v2.Property; @@ -101,8 +104,11 @@ public class UriServiceTest { @Test public void testPublicUrl() { + + String dataUri = "vos://example.com!vospace/mydata1"; Node node = new DataNode(); + node.setUri(dataUri); Property property = new Property(); property.setUri(NodeProperties.PUBLIC_READ_URI); property.setValue("true"); @@ -118,8 +124,11 @@ public class UriServiceTest { @Test public void testPrivateUrl() { + + String dataUri = "vos://example.com!vospace/mydata1"; Node node = new DataNode(); + node.setUri(dataUri); Property creator = new Property(); creator.setUri(NodeProperties.CREATOR_URI); creator.setValue("user1"); @@ -153,8 +162,11 @@ public class UriServiceTest { @Test public void testPrivateUrlPermissionDenied() { + + String dataUri = "vos://example.com!vospace/mydata1"; Node node = new DataNode(); + node.setUri(dataUri); Property creator = new Property(); creator.setUri(NodeProperties.CREATOR_URI); creator.setValue("user3"); @@ -188,8 +200,10 @@ public class UriServiceTest { @Test public void testPrivateUrlNodeBusy() { + String dataUri = "vos://example.com!vospace/mydata1"; DataNode node = new DataNode(); + node.setUri(dataUri); Property creator = new Property(); creator.setUri(NodeProperties.CREATOR_URI); creator.setValue("user1"); @@ -274,7 +288,115 @@ public class UriServiceTest { assertEquals("http://file-service/mydata1/mydata2?jobId=job-id2&token=<new-token>", negotiatedTransfer.getProtocols().get(0).getEndpoint()); } + + @Test + public void pullFromLinkNode() { + + // URI of pull target node + String targetOfPull = "vos://example.com!vospace/mylink1"; + String targetOfLink = "vos://example.com!vospace/mydummydata1"; + + // Define node properties + Property creator = new Property(); + creator.setUri(NodeProperties.CREATOR_URI); + creator.setValue("user1"); + + Property readgroup = new Property(); + readgroup.setUri(NodeProperties.GROUP_READ_URI); + readgroup.setValue("group1"); + + // Define link node as target + LinkNode lnode = new LinkNode(); + lnode.setUri(targetOfPull); + lnode.getProperties().add(creator); + lnode.getProperties().add(readgroup); + lnode.setTarget(targetOfLink); + + DataNode dnode = new DataNode(); + dnode.setUri(targetOfLink); + dnode.getProperties().add(creator); + dnode.getProperties().add(readgroup); + + when(nodeDAO.listNode(eq(NodeUtils.getVosPath(lnode)))).thenReturn(Optional.of(lnode)); + when(nodeDAO.listNode(eq(NodeUtils.getVosPath(dnode)))).thenReturn(Optional.of(dnode)); + + User user = mock(User.class); + when(user.getAccessToken()).thenReturn("<token>"); + 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); + + Transfer negotiatedTransfer = uriService.getNegotiatedTransfer(job, tr); + assertEquals("http://file-service" + + NodeUtils.getVosPath(dnode) + + "?jobId=job-id-pull&token=<new-token>", + negotiatedTransfer.getProtocols().get(0).getEndpoint()); + } + + @Test + public void pullFromCircularLinkNode() { + + // URI of pull target node + String targetOfPull = "vos://example.com!vospace/mylink1"; + String targetOfLink = "vos://example.com!vospace/mylink2"; + + // Define node properties + Property creator = new Property(); + creator.setUri(NodeProperties.CREATOR_URI); + creator.setValue("user1"); + + Property readgroup = new Property(); + readgroup.setUri(NodeProperties.GROUP_READ_URI); + readgroup.setValue("group1"); + + // Define link node as target + LinkNode lnode = new LinkNode(); + lnode.setUri(targetOfPull); + lnode.getProperties().add(creator); + lnode.getProperties().add(readgroup); + lnode.setTarget(targetOfLink); + + LinkNode dnode = new LinkNode(); + dnode.setUri(targetOfLink); + dnode.getProperties().add(creator); + dnode.getProperties().add(readgroup); + // Circular reference + dnode.setTarget(targetOfPull); + + when(nodeDAO.listNode(eq(NodeUtils.getVosPath(lnode)))).thenReturn(Optional.of(lnode)); + when(nodeDAO.listNode(eq(NodeUtils.getVosPath(dnode)))).thenReturn(Optional.of(dnode)); + + User user = mock(User.class); + when(user.getAccessToken()).thenReturn("<token>"); + 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); + + assertThrows(InternalFaultException.class, () -> { + uriService.getNegotiatedTransfer(job, tr); + }); + } + @Test public void setNodeRemoteLocationTest() { @@ -526,4 +648,23 @@ public class UriServiceTest { return job; } + + private JobSummary getPullFromVoSpaceJob(String target) { + Transfer transfer = new Transfer(); + transfer.setTarget(target); + transfer.setDirection(JobService.JobDirection.pullFromVoSpace.toString()); + Protocol protocol = new Protocol(); + protocol.setUri("ivo://ivoa.net/vospace/core#httpget"); + transfer.getProtocols().add(protocol); + + JobSummary job = new JobSummary(); + job.setJobId("job-id-pull"); + + JobSummary.JobInfo jobInfo = new JobSummary.JobInfo(); + jobInfo.getAny().add(transfer); + + job.setJobInfo(jobInfo); + + return job; + } }