From 7e8b01181910da200145313eb4f9d60a5a144b69 Mon Sep 17 00:00:00 2001 From: Nicola Fulvio Calabria <nicola.calabria@inaf.it> Date: Sun, 24 Oct 2021 21:32:43 +0200 Subject: [PATCH] Added link support to archives --- .../controller/ArchiveFileController.java | 6 +- .../ia2/transfer/persistence/FileDAO.java | 15 ++- .../transfer/persistence/model/FileInfo.java | 11 ++- .../ia2/transfer/service/ArchiveService.java | 96 ++++++++++++++++--- .../controller/ArchiveFileControllerTest.java | 2 +- .../transfer/service/ArchiveServiceTest.java | 12 ++- 6 files changed, 117 insertions(+), 25 deletions(-) diff --git a/src/main/java/it/inaf/ia2/transfer/controller/ArchiveFileController.java b/src/main/java/it/inaf/ia2/transfer/controller/ArchiveFileController.java index 755c529..f69ba9b 100644 --- a/src/main/java/it/inaf/ia2/transfer/controller/ArchiveFileController.java +++ b/src/main/java/it/inaf/ia2/transfer/controller/ArchiveFileController.java @@ -12,6 +12,7 @@ import it.inaf.ia2.transfer.service.ArchiveService; import it.inaf.oats.vospace.exception.PermissionDeniedException; import java.io.File; import java.util.concurrent.CompletableFuture; +import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpHeaders; @@ -29,6 +30,9 @@ public class ArchiveFileController extends AuthenticatedFileController { @Autowired private ArchiveService archiveService; + + @Autowired + private HttpServletRequest servletRequest; @Autowired private HttpServletResponse response; @@ -45,7 +49,7 @@ public class ArchiveFileController extends AuthenticatedFileController { job.setVosPaths(archiveRequest.getPaths()); CompletableFuture.runAsync(() -> { - handleFileJob(() -> archiveService.createArchive(job), job.getJobId()); + handleFileJob(() -> archiveService.createArchive(job, servletRequest), job.getJobId()); }); HttpHeaders headers = new HttpHeaders(); diff --git a/src/main/java/it/inaf/ia2/transfer/persistence/FileDAO.java b/src/main/java/it/inaf/ia2/transfer/persistence/FileDAO.java index cff63fa..b04847b 100644 --- a/src/main/java/it/inaf/ia2/transfer/persistence/FileDAO.java +++ b/src/main/java/it/inaf/ia2/transfer/persistence/FileDAO.java @@ -47,7 +47,7 @@ public class FileDAO { + "accept_views, provide_views, l.location_type, n.path <> n.relative_path AS virtual_parent,\n" + "(SELECT user_name FROM users WHERE user_id = creator_id) AS username, n.job_id,\n" + "base_path, get_os_path(n.node_id) AS os_path, ? AS vos_path, false AS is_directory,\n" - + "type = 'link' AS is_link,\n" + + "n.type = 'link' AS is_link, n.target,\n" + "fs_path \n" + "FROM node n\n" + "JOIN location l ON (n.location_id IS NOT NULL AND n.location_id = l.location_id) OR (n.location_id IS NULL AND l.location_id = ?)\n" @@ -180,7 +180,7 @@ public class FileDAO { + "(SELECT user_name FROM users WHERE user_id = n.creator_id) AS username,\n" + "base_path, get_os_path(n.node_id) AS os_path, get_vos_path(n.node_id) AS vos_path,\n" + "n.type = 'container' AS is_directory, n.name, n.location_id, n.job_id,\n" - + "n.type = 'link' AS is_link, l.location_type\n" + + "n.type = 'link' AS is_link, n.target, l.location_type\n" + "FROM node n\n" + "JOIN node p ON p.path @> n.path\n" + "LEFT JOIN location l ON l.location_id = n.location_id\n" @@ -213,7 +213,7 @@ public class FileDAO { + "(SELECT user_name FROM users WHERE user_id = n.creator_id) AS username,\n" + "base_path, get_os_path(n.node_id) AS os_path, get_vos_path(n.node_id) AS vos_path,\n" + "n.type = 'container' AS is_directory, n.name, n.location_id, n.job_id,\n" - + "n.type = 'link' AS is_link, l.location_type\n" + + "n.type = 'link' AS is_link, n.target, l.location_type\n" + "FROM node n\n" + "JOIN node p ON p.path @> n.path\n" + "LEFT JOIN location l ON l.location_id = n.location_id\n" @@ -289,11 +289,16 @@ public class FileDAO { long contentLength = rs.getLong("content_length"); if (!rs.wasNull()) { fi.setContentLength(contentLength); - } + } fi.setContentMd5(rs.getString("content_md5")); fi.setContentType(rs.getString("content_type")); fi.setDirectory(rs.getBoolean("is_directory")); - fi.setLink(rs.getBoolean("is_link")); + if(rs.getBoolean("is_link")){ + fi.setLink(true); + fi.setTarget(rs.getString("target")); + } else { + fi.setLink(false); + } fi.setJobId(rs.getString("job_id")); int locationId = rs.getInt("location_id"); if (!rs.wasNull()) { diff --git a/src/main/java/it/inaf/ia2/transfer/persistence/model/FileInfo.java b/src/main/java/it/inaf/ia2/transfer/persistence/model/FileInfo.java index f0b032d..4081d07 100644 --- a/src/main/java/it/inaf/ia2/transfer/persistence/model/FileInfo.java +++ b/src/main/java/it/inaf/ia2/transfer/persistence/model/FileInfo.java @@ -17,11 +17,12 @@ public class FileInfo { private String fsPath; // actualBasePath differs from base path in db due to some location type // dependent manipulations (performed by FileDAO) - private String actualBasePath; + private String actualBasePath; private boolean isPublic; private boolean virtualParent; private boolean directory; private boolean link; + private String target; private List<String> groupRead; private List<String> groupWrite; private String ownerId; @@ -36,6 +37,14 @@ public class FileInfo { private String locationType; private String jobId; + public String getTarget() { + return target; + } + + public void setTarget(String target) { + this.target = target; + } + public int getNodeId() { return nodeId; } diff --git a/src/main/java/it/inaf/ia2/transfer/service/ArchiveService.java b/src/main/java/it/inaf/ia2/transfer/service/ArchiveService.java index 2a98e89..a273d77 100644 --- a/src/main/java/it/inaf/ia2/transfer/service/ArchiveService.java +++ b/src/main/java/it/inaf/ia2/transfer/service/ArchiveService.java @@ -5,6 +5,9 @@ */ package it.inaf.ia2.transfer.service; +import it.inaf.ia2.aa.ServletRapClient; +import it.inaf.ia2.aa.data.User; +import it.inaf.ia2.rap.client.call.TokenExchangeRequest; import it.inaf.ia2.transfer.auth.TokenPrincipal; import it.inaf.ia2.transfer.persistence.FileDAO; import it.inaf.ia2.transfer.persistence.JobDAO; @@ -13,6 +16,7 @@ import it.inaf.ia2.transfer.persistence.model.FileInfo; import it.inaf.oats.vospace.exception.InternalFaultException; import it.inaf.oats.vospace.exception.PermissionDeniedException; import it.inaf.oats.vospace.exception.QuotaExceededException; +import it.inaf.oats.vospace.parent.persistence.LinkedServiceDAO; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileInputStream; @@ -28,6 +32,7 @@ import java.util.Map; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; import javax.annotation.PostConstruct; +import javax.servlet.http.HttpServletRequest; import net.ivoa.xml.uws.v1.ExecutionPhase; import org.kamranzafar.jtar.TarEntry; import org.kamranzafar.jtar.TarOutputStream; @@ -52,6 +57,9 @@ public class ArchiveService { @Autowired private LocationDAO locationDAO; + + @Autowired + private LinkedServiceDAO linkedServiceDAO; @Autowired private JobDAO jobDAO; @@ -61,6 +69,9 @@ public class ArchiveService { @Autowired private RestTemplate restTemplate; + + @Autowired + private ServletRapClient rapClient; @Value("${upload_location_id}") private int uploadLocationId; @@ -84,7 +95,7 @@ public class ArchiveService { } } - public <O extends OutputStream, E> void createArchive(ArchiveJob job) { + public <O extends OutputStream, E> void createArchive(ArchiveJob job, HttpServletRequest servletRequest) { jobDAO.updateJobPhase(ExecutionPhase.EXECUTING, job.getJobId()); @@ -101,7 +112,7 @@ public class ArchiveService { // it will be initialized only when necessary Map<Integer, String> portalLocationUrls = null; - try ( ArchiveHandler<O, E> handler = getArchiveHandler(archiveFile, job.getType())) { + try (ArchiveHandler<O, E> handler = getArchiveHandler(archiveFile, job.getType())) { for (FileInfo fileInfo : fileDAO.getArchiveFileInfos(job.getVosPaths())) { @@ -112,13 +123,21 @@ public class ArchiveService { continue; } + // I expect only external links + // local links have been resolved before calling this endpoint + if (fileInfo.isLink()) { + downloadExternalLinkIntoArchive(fileInfo, relPath, + job.getPrincipal(), handler, servletRequest); + continue; + } + if (fileInfo.getLocationId() != null && "portal".equals(fileInfo.getLocationType())) { // remote file if (portalLocationUrls == null) { portalLocationUrls = locationDAO.getPortalLocationUrls(); } String url = portalLocationUrls.get(fileInfo.getLocationId()); - downloadFileIntoArchive(fileInfo, relPath, job.getPrincipal(), handler, url); + downloadRemoteLocationFileIntoArchive(fileInfo, relPath, job.getPrincipal(), handler, url); } else { // local file or virtual directory writeFileIntoArchive(fileInfo, relPath, job.getPrincipal(), handler); @@ -272,15 +291,7 @@ public class ArchiveService { } } - private <O extends OutputStream, E> void downloadFileIntoArchive(FileInfo fileInfo, String relPath, TokenPrincipal tokenPrincipal, ArchiveHandler<O, E> handler, String baseUrl) { - - if (baseUrl == null) { - LOG.error("Location URL not found for location " + fileInfo.getLocationId()); - throw new InternalFaultException("Unable to retrieve location of file " + fileInfo.getVirtualPath()); - } - - String url = baseUrl + "/" + fileInfo.getVirtualName(); - + private <O extends OutputStream, E> void downloadFromUrlIntoArchive(String url, String relPath, TokenPrincipal tokenPrincipal, ArchiveHandler<O, E> handler) { LOG.trace("Downloading file from " + url); restTemplate.execute(url, HttpMethod.GET, req -> { @@ -290,10 +301,10 @@ public class ArchiveService { } }, res -> { File tmpFile = Files.createTempFile("download", null).toFile(); - try ( FileOutputStream os = new FileOutputStream(tmpFile)) { + try (FileOutputStream os = new FileOutputStream(tmpFile)) { res.getBody().transferTo(os); handler.putNextEntry(tmpFile, relPath); - try ( FileInputStream is = new FileInputStream(tmpFile)) { + try (FileInputStream is = new FileInputStream(tmpFile)) { is.transferTo(handler.getOutputStream()); } } finally { @@ -303,6 +314,43 @@ public class ArchiveService { }, new Object[]{}); } + private <O extends OutputStream, E> void downloadRemoteLocationFileIntoArchive( + FileInfo fileInfo, String relPath, TokenPrincipal tokenPrincipal, + ArchiveHandler<O, E> handler, String baseUrl) { + + if (baseUrl == null) { + LOG.error("Location URL not found for location " + fileInfo.getLocationId()); + throw new InternalFaultException("Unable to retrieve location of file " + + fileInfo.getVirtualPath()); + } + + String url = baseUrl + "/" + fileInfo.getVirtualName(); + + downloadFromUrlIntoArchive(url, relPath, tokenPrincipal, handler); + } + + private <O extends OutputStream, E> void downloadExternalLinkIntoArchive( + FileInfo fileInfo, String relPath, TokenPrincipal tokenPrincipal, + ArchiveHandler<O, E> handler, HttpServletRequest servletRequest) { + + String url = fileInfo.getTarget(); + + if (url == null || url.isBlank()) { + LOG.error("Target URL of link at path: {} is null or blank", fileInfo.getVirtualPath()); + throw new InternalFaultException("Target URL of link at path: " + + fileInfo.getVirtualPath() + " is null or blank"); + } + + // Append token if url is recognized + + if (linkedServiceDAO.isLinkedServiceUrl(url)) { + url += "?token=" + getEndpointToken(tokenPrincipal, url, servletRequest); + } + + downloadFromUrlIntoArchive(url, relPath, tokenPrincipal, handler); + + } + private <O extends OutputStream, E> void writeFileIntoArchive(FileInfo fileInfo, String relPath, TokenPrincipal tokenPrincipal, ArchiveHandler<O, E> handler) throws IOException { if (!authorizationService.isDownloadable(fileInfo, tokenPrincipal)) { throw PermissionDeniedException.forPath(fileInfo.getVirtualPath()); @@ -311,9 +359,27 @@ public class ArchiveService { File file = new File(fileInfo.getFilePath()); LOG.trace("Adding file " + file.getAbsolutePath() + " to tar archive"); - try ( InputStream is = new FileInputStream(file)) { + try (InputStream is = new FileInputStream(file)) { handler.putNextEntry(file, relPath); is.transferTo(handler.getOutputStream()); } } + + private String getEndpointToken(TokenPrincipal tokenPrincipal, + String endpoint, HttpServletRequest servletRequest) { + + String token = tokenPrincipal.getToken(); + + if (token == null) { + throw new PermissionDeniedException("Token is null"); + } + + TokenExchangeRequest exchangeRequest = new TokenExchangeRequest() + .setSubjectToken(token) + .setResource(endpoint); + + // TODO: add audience and scope + return rapClient.exchangeToken(exchangeRequest, servletRequest); + } + } diff --git a/src/test/java/it/inaf/ia2/transfer/controller/ArchiveFileControllerTest.java b/src/test/java/it/inaf/ia2/transfer/controller/ArchiveFileControllerTest.java index b5d04fd..764322f 100644 --- a/src/test/java/it/inaf/ia2/transfer/controller/ArchiveFileControllerTest.java +++ b/src/test/java/it/inaf/ia2/transfer/controller/ArchiveFileControllerTest.java @@ -65,7 +65,7 @@ public class ArchiveFileControllerTest { assertEquals("user1", job.getPrincipal().getName()); assertEquals(2, job.getVosPaths().size()); return true; - })); + }), any()); } @Test diff --git a/src/test/java/it/inaf/ia2/transfer/service/ArchiveServiceTest.java b/src/test/java/it/inaf/ia2/transfer/service/ArchiveServiceTest.java index 53f7bc9..081e1ac 100644 --- a/src/test/java/it/inaf/ia2/transfer/service/ArchiveServiceTest.java +++ b/src/test/java/it/inaf/ia2/transfer/service/ArchiveServiceTest.java @@ -26,6 +26,7 @@ import java.util.Map; import java.util.function.Function; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; +import javax.servlet.http.HttpServletRequest; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Assertions; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -69,6 +70,9 @@ public class ArchiveServiceTest { @MockBean private RestTemplate restTemplate; + + @MockBean + private HttpServletRequest servletRequest; @MockBean private AuthorizationService authorizationService; @@ -139,6 +143,8 @@ public class ArchiveServiceTest { job.setJobId("job2"); job.setType(ArchiveJob.Type.ZIP); job.setVosPaths(Arrays.asList("/ignore")); + + when(servletRequest.getUserPrincipal()).thenReturn(job.getPrincipal()); File user2Dir = tmpDir.toPath().resolve("user2").toFile(); user2Dir.mkdir(); @@ -153,7 +159,7 @@ public class ArchiveServiceTest { } Assertions.assertThrows(QuotaExceededException.class, () -> { - archiveService.createArchive(job); + archiveService.createArchive(job, servletRequest); }); } private static abstract class TestArchiveHandler<I extends InputStream, E> { @@ -193,6 +199,8 @@ public class ArchiveServiceTest { job.setJobId("abcdef"); job.setType(type); job.setVosPaths(Arrays.asList(parent + "/dir1", parent + "/dir2", parent + "/file6")); + + when(servletRequest.getUserPrincipal()).thenReturn(job.getPrincipal()); when(authorizationService.isDownloadable(any(), any())).thenReturn(true); @@ -224,7 +232,7 @@ public class ArchiveServiceTest { }).when(restTemplate).execute(eq("http://portal/base/url/portal-file"), eq(HttpMethod.GET), any(RequestCallback.class), any(ResponseExtractor.class), any(Object[].class)); - archiveService.createArchive(job); + archiveService.createArchive(job, servletRequest); File result = tmpDir.toPath().resolve("user1").resolve("abcdef." + extension).toFile(); -- GitLab