diff --git a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/SessionThreadFactory.java b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/SessionThreadFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..ad030d347589313db481ff251b174b21f0c4db0c --- /dev/null +++ b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/SessionThreadFactory.java @@ -0,0 +1,53 @@ +/* + * This file is part of vospace-ui + * Copyright (C) 2021 Istituto Nazionale di Astrofisica + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package it.inaf.ia2.vospace.ui; + +import java.util.concurrent.ThreadFactory; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpSession; + +/** + * VOSpaceClient needs to extract the access token stored into the HTTP session + * and forward it to the VOSpace REST service. The client can retrieve the + * session from the autowired HttpServletRequest, however this doesn't work if + * the client is called from a standard new thread (for example when the client + * is invoked from a CompletableFuture), because Spring Context is missing and + * retrieval of the autowired HttpServletRequest proxy fails with the "No + * thread-bound request found" exception. Many CompletableFuture methods accept + * an Executor as parameter, so this ThreadFactory has been created in order to + * store the session in these particular threads. An Executor using this + * ThreadaFactory must be passed to the CompletableFuture. An alternative would + * be passing the original HttpServletRequest to all VOSpaceClient methods. A + * first attempt using the current executor (Runnable::run) shown that tasks + * where executed sequentially, so this dedicated Executor has been set up. + */ +public class SessionThreadFactory implements ThreadFactory { + + private final HttpServletRequest request; + + public SessionThreadFactory(HttpServletRequest request) { + this.request = request; + } + + @Override + public Thread newThread(Runnable runnable) { + return new SessionThread(runnable, request); + } + + public static class SessionThread extends Thread { + + private final HttpSession session; + + public SessionThread(Runnable runnable, HttpServletRequest request) { + super(runnable); + this.session = request.getSession(false); + } + + public HttpSession getHttpSession() { + return this.session; + } + } +} diff --git a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/TokenProvider.java b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/TokenProvider.java new file mode 100644 index 0000000000000000000000000000000000000000..f494da5b8494904c456baf60ac3ccf638379db33 --- /dev/null +++ b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/TokenProvider.java @@ -0,0 +1,32 @@ +/* + * This file is part of vospace-ui + * Copyright (C) 2021 Istituto Nazionale di Astrofisica + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package it.inaf.ia2.vospace.ui; + +import it.inaf.ia2.aa.data.User; +import java.util.Optional; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpSession; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public class TokenProvider { + + @Autowired + private HttpServletRequest request; + + public Optional<String> getToken() { + + HttpSession session = request.getSession(false); + if (session != null) { + User user = (User) session.getAttribute("user_data"); + if (user != null) { + return Optional.of(user.getAccessToken()); + } + } + return Optional.empty(); + } +} diff --git a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/VOSpaceUiApplication.java b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/VOSpaceUiApplication.java index f251deba7529a664c3d2d05d70526b7884b644fd..2ca025d6c19e163451cef466cf2d9f20b299de9d 100644 --- a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/VOSpaceUiApplication.java +++ b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/VOSpaceUiApplication.java @@ -12,6 +12,8 @@ import it.inaf.ia2.aa.ServletRapClient; import it.inaf.ia2.aa.UserManager; import it.inaf.ia2.gms.client.GmsClient; import it.inaf.ia2.rap.client.ClientCredentialsRapClient; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; import java.util.concurrent.ForkJoinPool; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @@ -66,6 +68,16 @@ public class VOSpaceUiApplication { return new ForkJoinPool(parallelism, threadFactory, null, false); } + /** + * Executor to pass to CompletableFuture methods to avoid + * "RejectedExecutionException: Thread limit exceeded replacing blocked + * worker". + */ + @Bean + public Executor requestsExecutor() { + return Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()); + } + @Bean public RestTemplate restTemplate() { return new RestTemplate(); diff --git a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/client/VOSpaceClient.java b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/client/VOSpaceClient.java index 4a33a42c2a5052b28686c7cc05b4190d08bd057d..690a3cbaeec03ef3b4679d8e0f5f7b4a220b9a15 100644 --- a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/client/VOSpaceClient.java +++ b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/client/VOSpaceClient.java @@ -6,7 +6,6 @@ package it.inaf.ia2.vospace.ui.client; import com.fasterxml.jackson.databind.ObjectMapper; -import it.inaf.ia2.aa.data.User; import it.inaf.ia2.vospace.ui.VOSpaceUiApplication; import it.inaf.ia2.vospace.ui.data.Job; import it.inaf.ia2.vospace.ui.exception.BadRequestException; @@ -35,8 +34,6 @@ import java.util.function.Function; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpSession; import javax.xml.bind.JAXB; import net.ivoa.xml.uws.v1.ExecutionPhase; import net.ivoa.xml.uws.v1.JobSummary; @@ -46,7 +43,6 @@ import net.ivoa.xml.vospace.v2.Protocol; import net.ivoa.xml.vospace.v2.Transfer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; @@ -64,9 +60,6 @@ public class VOSpaceClient { private final String baseUrl; private final ForkJoinPool jaxbExecutor; - @Autowired - protected HttpServletRequest servletRequest; - public VOSpaceClient(@Value("${vospace-backend-url}") String backendUrl) { if (backendUrl.endsWith("/")) { // Remove final slash from configured URL @@ -82,22 +75,18 @@ public class VOSpaceClient { .build(); } - public Node getNode(String path) { - return getNode(path, Optional.empty()); - } - - public Node getNode(String path, Optional<String> adminToken) { + public Node getNode(String path, Optional<String> token) { - HttpRequest request = getRequest("/nodes" + urlEncodePath(path), adminToken) + HttpRequest request = getRequest("/nodes" + urlEncodePath(path), token) .header("Accept", useJson ? "application/json" : "text/xml") .build(); return call(request, BodyHandlers.ofInputStream(), 200, res -> unmarshal(res, Node.class)); } - public JobSummary startTransferJob(Transfer transfer) { + public JobSummary startTransferJob(Transfer transfer, Optional<String> token) { - HttpRequest request = getRequest("/transfers?PHASE=RUN") + HttpRequest request = getRequest("/transfers?PHASE=RUN", token) .header("Accept", useJson ? "application/json" : "text/xml") .header("Content-Type", useJson ? "application/json" : "text/xml") .POST(HttpRequest.BodyPublishers.ofString(marshal(transfer))) @@ -106,9 +95,9 @@ public class VOSpaceClient { return call(request, BodyHandlers.ofInputStream(), 200, res -> unmarshal(res, JobSummary.class)); } - public String getFileServiceEndpoint(Transfer transfer) { + public String getFileServiceEndpoint(Transfer transfer, Optional<String> token) { - HttpRequest request = getRequest("/synctrans") + HttpRequest request = getRequest("/synctrans", token) .header("Accept", useJson ? "application/json" : "text/xml") .header("Content-Type", useJson ? "application/json" : "text/xml") .POST(HttpRequest.BodyPublishers.ofString(marshal(transfer))) @@ -131,7 +120,7 @@ public class VOSpaceClient { Matcher matcher = pattern.matcher(url); if (matcher.matches()) { String jobId = matcher.group(1); - String errorDetail = getErrorDetail(jobId); + String errorDetail = getErrorDetail(jobId, token); if (!errorDetail.isBlank()) { throw new BadRequestException(errorDetail); } @@ -142,15 +131,11 @@ public class VOSpaceClient { return protocols.get(0).getEndpoint(); } - public Node createNode(Node node) { - return createNode(node, Optional.empty()); - } - - public Node createNode(Node node, Optional<String> adminToken) { + public Node createNode(Node node, Optional<String> token) { String path = NodeUtils.getVosPath(node); - HttpRequest request = getRequest("/nodes" + urlEncodePath(path), adminToken) + HttpRequest request = getRequest("/nodes" + urlEncodePath(path), token) .header("Accept", useJson ? "application/json" : "text/xml") .header("Content-Type", useJson ? "application/json" : "text/xml") .PUT(HttpRequest.BodyPublishers.ofString(marshal(node))) @@ -159,9 +144,9 @@ public class VOSpaceClient { return call(request, BodyHandlers.ofInputStream(), 200, res -> unmarshal(res, Node.class)); } - public void deleteNode(String path) { + public void deleteNode(String path, Optional<String> token) { - HttpRequest request = getRequest("/nodes" + urlEncodePath(path)) + HttpRequest request = getRequest("/nodes" + urlEncodePath(path), token) .header("Accept", useJson ? "application/json" : "text/xml") .header("Content-Type", useJson ? "application/json" : "text/xml") .DELETE() @@ -170,15 +155,11 @@ public class VOSpaceClient { call(request, BodyHandlers.ofInputStream(), 200, res -> null); } - public Node setNode(Node node, boolean recursive) { - return setNode(node, recursive, Optional.empty()); - } - - public Node setNode(Node node, boolean recursive, Optional<String> adminToken) { + public Node setNode(Node node, boolean recursive, Optional<String> token) { String path = NodeUtils.getVosPath(node); - HttpRequest request = getRequest("/nodes" + urlEncodePath(path) + "?recursive=" + recursive, adminToken) + HttpRequest request = getRequest("/nodes" + urlEncodePath(path) + "?recursive=" + recursive, token) .header("Accept", useJson ? "application/json" : "text/xml") .header("Content-Type", useJson ? "application/json" : "text/xml") .POST(HttpRequest.BodyPublishers.ofString(marshal(node))) @@ -187,20 +168,20 @@ public class VOSpaceClient { return call(request, BodyHandlers.ofInputStream(), 200, res -> unmarshal(res, Node.class)); } - public List<Job> getAsyncRecallJobs() { - return getJobs("direction=pullToVoSpace", Job.JobType.ASYNC_RECALL); + public List<Job> getAsyncRecallJobs(Optional<String> token) { + return getJobs("direction=pullToVoSpace", Job.JobType.ASYNC_RECALL, token); } - public List<Job> getArchiveJobs() { + public List<Job> getArchiveJobs(Optional<String> token) { return getJobs("direction=pullFromVoSpace" + "&VIEW=ivo://ia2.inaf.it/vospace/views%23tar" + "&VIEW=ivo://ia2.inaf.it/vospace/views%23zip", - Job.JobType.ARCHIVE); + Job.JobType.ARCHIVE, token); } - private List<Job> getJobs(String queryString, Job.JobType type) { + private List<Job> getJobs(String queryString, Job.JobType type, Optional<String> token) { - HttpRequest request = getRequest("/transfers?" + queryString) + HttpRequest request = getRequest("/transfers?" + queryString, token) .header("Accept", useJson ? "application/json" : "text/xml") .header("Content-Type", useJson ? "application/json" : "text/xml") .GET() @@ -213,25 +194,25 @@ public class VOSpaceClient { }); } - public String getArchiveJobHref(String jobId) { - List<Protocol> protocols = getTransferDetails(jobId).getProtocols(); + public String getArchiveJobHref(String jobId, Optional<String> token) { + List<Protocol> protocols = getTransferDetails(jobId, token).getProtocols(); if (!protocols.isEmpty()) { return protocols.get(0).getEndpoint(); } return null; } - private Transfer getTransferDetails(String jobId) { + private Transfer getTransferDetails(String jobId, Optional<String> token) { - HttpRequest request = getRequest("/transfers/" + jobId + "/results/transferDetails") + HttpRequest request = getRequest("/transfers/" + jobId + "/results/transferDetails", token) .GET().build(); return call(request, BodyHandlers.ofInputStream(), 200, res -> unmarshal(res, Transfer.class)); } - public ExecutionPhase getJobPhase(String jobId) { + public ExecutionPhase getJobPhase(String jobId, Optional<String> token) { - HttpRequest request = getRequest("/transfers/" + jobId + "/phase") + HttpRequest request = getRequest("/transfers/" + jobId + "/phase", token) .GET() .build(); @@ -244,9 +225,9 @@ public class VOSpaceClient { }); } - public String getErrorDetail(String jobId) { + public String getErrorDetail(String jobId, Optional<String> token) { - HttpRequest request = getRequest("/transfers/" + jobId + "/error") + HttpRequest request = getRequest("/transfers/" + jobId + "/error", token) .header("Accept", "text/plain") .GET() .build(); @@ -291,30 +272,14 @@ public class VOSpaceClient { } } - private HttpRequest.Builder getRequest(String path) { - return getRequest(path, Optional.empty()); - } - - private HttpRequest.Builder getRequest(String path, Optional<String> adminToken) { + private HttpRequest.Builder getRequest(String path, Optional<String> token) { HttpRequest.Builder builder = HttpRequest.newBuilder(URI.create(baseUrl + path)); - String token = adminToken.orElseGet(() -> getToken()); - if (token != null) { - builder.setHeader("Authorization", "Bearer " + token); + if (token.isPresent()) { + builder.setHeader("Authorization", "Bearer " + token.get()); } return builder; } - private String getToken() { - HttpSession session = servletRequest.getSession(false); - if (session != null) { - User user = (User) session.getAttribute("user_data"); - if (user != null) { - return user.getAccessToken(); - } - } - return null; - } - private <T> T unmarshal(InputStream in, Class<T> type) { try { if (useJson) { diff --git a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/CreateLinksController.java b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/CreateLinksController.java index b3dfe7fc7e1e36bfe45dbbde519cc426c8933c6b..2e1700d4cb3e5350042ee109515ace8a8f511a76 100644 --- a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/CreateLinksController.java +++ b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/CreateLinksController.java @@ -5,6 +5,7 @@ */ package it.inaf.ia2.vospace.ui.controller; +import it.inaf.ia2.vospace.ui.TokenProvider; import it.inaf.ia2.vospace.ui.client.VOSpaceClient; import it.inaf.ia2.vospace.ui.data.CreateLinkRequest; import it.inaf.ia2.vospace.ui.exception.BadRequestException; @@ -14,7 +15,9 @@ import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; import java.util.List; +import java.util.Optional; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; import net.ivoa.xml.vospace.v2.ContainerNode; import net.ivoa.xml.vospace.v2.LinkNode; import org.slf4j.Logger; @@ -40,10 +43,18 @@ public class CreateLinksController extends BaseController { @Autowired private VOSpaceClient client; + @Autowired + private TokenProvider tokenProvider; + + @Autowired + private Executor requestsExecutor; + @PostMapping(value = "/createLink", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity<?> createLink(@RequestBody CreateLinkRequest request) { - ContainerNode parent = getFolder(request.getFolder()); + Optional<String> token = tokenProvider.getToken(); + + ContainerNode parent = getFolder(request.getFolder(), token); String uri = parent.getUri() + "/" + request.getNodeName(); @@ -51,7 +62,7 @@ public class CreateLinksController extends BaseController { link.setUri(uri); link.setTarget(request.getUrl()); - client.createNode(link); + client.createNode(link, token); return ResponseEntity.noContent().build(); } @@ -60,7 +71,9 @@ public class CreateLinksController extends BaseController { public ResponseEntity<?> uploadLinks(@RequestParam(value = "file", required = true) MultipartFile file, @RequestParam("folder") String folder) throws IOException { - ContainerNode parent = getFolder(folder); + Optional<String> token = tokenProvider.getToken(); + + ContainerNode parent = getFolder(folder, token); String fileContent = new String(file.getBytes()); @@ -90,7 +103,7 @@ public class CreateLinksController extends BaseController { httpCallsGroups.add(currentHttpCallsGroup); } - currentHttpCallsGroup.add(CompletableFuture.supplyAsync(() -> client.createNode(link), Runnable::run)); + currentHttpCallsGroup.add(CompletableFuture.supplyAsync(() -> client.createNode(link, token), requestsExecutor)); } } @@ -123,9 +136,9 @@ public class CreateLinksController extends BaseController { } } - private ContainerNode getFolder(String folderPath) { + private ContainerNode getFolder(String folderPath, Optional<String> token) { try { - return (ContainerNode) client.getNode("/" + folderPath); + return (ContainerNode) client.getNode("/" + folderPath, token); } catch (VOSpaceStatusException ex) { if (ex.getHttpStatus() == 404) { throw new BadRequestException("Folder parameter specified a non-existent folder: /" + folderPath); diff --git a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/DownloadController.java b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/DownloadController.java index 381dd858e7194af5a8d009d5a1965fa2fe563ce4..faaf782042c926c73233ca1c2cf36d83ec7e28c0 100644 --- a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/DownloadController.java +++ b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/DownloadController.java @@ -8,6 +8,7 @@ package it.inaf.ia2.vospace.ui.controller; 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.vospace.ui.TokenProvider; import it.inaf.ia2.vospace.ui.client.VOSpaceClient; import it.inaf.ia2.vospace.ui.exception.PermissionDeniedException; import it.inaf.oats.vospace.datamodel.NodeUtils; @@ -43,6 +44,9 @@ public class DownloadController { @Autowired private ServletRapClient rapClient; + @Autowired + private TokenProvider tokenProvider; + @GetMapping(value = "/download/**") public ResponseEntity<?> directDownload() { @@ -61,7 +65,7 @@ public class DownloadController { httpsProtocol.setUri("ivo://ivoa.net/vospace/core#httpsget"); transfer.getProtocols().add(httpsProtocol); - String url = client.getFileServiceEndpoint(transfer); + String url = client.getFileServiceEndpoint(transfer, tokenProvider.getToken()); HttpHeaders headers = new HttpHeaders(); headers.set("Location", url); return new ResponseEntity<>(headers, HttpStatus.SEE_OTHER); @@ -78,7 +82,7 @@ public class DownloadController { throw new PermissionDeniedException("Token is null"); } - String endpoint = client.getArchiveJobHref(jobId); + String endpoint = client.getArchiveJobHref(jobId, tokenProvider.getToken()); TokenExchangeRequest exchangeRequest = new TokenExchangeRequest() .setSubjectToken(token) diff --git a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/JobController.java b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/JobController.java index d185feb8cf513af4035b2edeecf389b13563147b..87fb86559e94c431cb5c7354a86ba1674e472cc3 100644 --- a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/JobController.java +++ b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/JobController.java @@ -5,13 +5,16 @@ */ package it.inaf.ia2.vospace.ui.controller; +import it.inaf.ia2.vospace.ui.TokenProvider; import it.inaf.ia2.vospace.ui.client.VOSpaceClient; import it.inaf.ia2.vospace.ui.data.Job; import it.inaf.ia2.vospace.ui.exception.BadRequestException; import it.inaf.oats.vospace.datamodel.Views; import java.util.ArrayList; import java.util.List; +import java.util.Optional; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; import net.ivoa.xml.uws.v1.ErrorType; import net.ivoa.xml.uws.v1.ExecutionPhase; import net.ivoa.xml.uws.v1.JobSummary; @@ -40,6 +43,12 @@ public class JobController extends BaseController { @Autowired private VOSpaceClient client; + @Autowired + private TokenProvider tokenProvider; + + @Autowired + private Executor requestsExecutor; + @PostMapping(value = "/recall", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity<Job> startRecallFromTapeJob(@RequestBody List<String> paths) { @@ -67,7 +76,9 @@ public class JobController extends BaseController { } } - JobSummary job = client.startTransferJob(transfer); + Optional<String> token = tokenProvider.getToken(); + + JobSummary job = client.startTransferJob(transfer, token); if (job.getPhase() == ExecutionPhase.QUEUED || job.getPhase() == ExecutionPhase.PENDING @@ -78,7 +89,7 @@ public class JobController extends BaseController { if (job.getPhase() == ExecutionPhase.ERROR) { if (job.getErrorSummary() != null) { if (job.getErrorSummary().isHasDetail()) { - errorMessage = client.getErrorDetail(job.getJobId()); + errorMessage = client.getErrorDetail(job.getJobId(), token); } else { errorMessage = job.getErrorSummary().getMessage(); } @@ -102,13 +113,13 @@ public class JobController extends BaseController { @GetMapping(value = "/jobs", produces = MediaType.APPLICATION_JSON_VALUE) public List<Job> getJobs() throws Exception { + Optional<String> token = tokenProvider.getToken(); + CompletableFuture<List<Job>> asyncRecallJobsCall - = CompletableFuture.supplyAsync(() -> client.getAsyncRecallJobs(), - Runnable::run); + = CompletableFuture.supplyAsync(() -> client.getAsyncRecallJobs(token), requestsExecutor); CompletableFuture<List<Job>> archiveJobsCall - = CompletableFuture.supplyAsync(() -> client.getArchiveJobs(), - Runnable::run); + = CompletableFuture.supplyAsync(() -> client.getArchiveJobs(token), requestsExecutor); CompletableFuture.allOf(asyncRecallJobsCall, archiveJobsCall).join(); diff --git a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/NodesController.java b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/NodesController.java index 50be555ca32b26f7a02967437a662f64cbb4b0cd..f0fa03564b39a4d9544801bce372311bd66d79d6 100644 --- a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/NodesController.java +++ b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/NodesController.java @@ -6,6 +6,7 @@ package it.inaf.ia2.vospace.ui.controller; import it.inaf.ia2.aa.data.User; +import it.inaf.ia2.vospace.ui.TokenProvider; import it.inaf.ia2.vospace.ui.client.VOSpaceClient; import it.inaf.ia2.vospace.ui.data.Job; import it.inaf.ia2.vospace.ui.data.ListNodeData; @@ -26,6 +27,7 @@ import java.util.Map; import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; +import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -70,6 +72,12 @@ public class NodesController extends BaseController { @Autowired private HttpServletRequest servletRequest; + @Autowired + private TokenProvider tokenProvider; + + @Autowired + private Executor requestsExecutor; + @GetMapping(value = {"/nodes", "/nodes/**"}, produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity<ListNodeData> listNodes(User principal) throws Exception { @@ -80,7 +88,7 @@ public class NodesController extends BaseController { Node node; try { - node = client.getNode(path); + node = client.getNode(path, tokenProvider.getToken()); } catch (VOSpaceStatusException ex) { if (ex.getHttpStatus() == 403) { String message = "You cannot access this node"; @@ -109,7 +117,7 @@ public class NodesController extends BaseController { ListNodeData listNodeData = new ListNodeData(); - Node node = client.getNode(path); + Node node = client.getNode(path, tokenProvider.getToken()); listNodeData.setWritable(NodeUtils.checkIfWritable(node, principal.getName(), principal.getGroups())); @@ -133,13 +141,15 @@ public class NodesController extends BaseController { // a container, a VOSpace file or an external file List<CompletableFuture<Node>> nodesCalls = new ArrayList<>(); + Optional<String> token = tokenProvider.getToken(); + for (LinkNode linkNode : linksToLoad) { String prefix = "vos://" + authority; if (linkNode.getTarget().startsWith(prefix)) { String linkPath = StringUtils.uriDecode(linkNode.getTarget(), StandardCharsets.UTF_8) .substring(prefix.length()); - nodesCalls.add(CompletableFuture.supplyAsync(() -> client.getNode(linkPath), Runnable::run) + nodesCalls.add(CompletableFuture.supplyAsync(() -> client.getNode(linkPath, token), requestsExecutor) .exceptionally(ex -> null)); // null is returned in case of broken link } else { linkedNodes.add(linkNode); @@ -183,18 +193,20 @@ public class NodesController extends BaseController { creator.setValue(getUser().getName()); node.getProperties().add(creator); - client.createNode(node); + client.createNode(node, tokenProvider.getToken()); } @PostMapping(value = "/delete", consumes = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity<?> deleteNodes(@RequestBody List<String> nodesToDelete) { + Optional<String> token = tokenProvider.getToken(); + CompletableFuture[] deleteCalls = nodesToDelete.stream() .map(nodeToDelete -> { return CompletableFuture.runAsync(() -> { LOG.debug("deleteNode called for path {}", nodeToDelete); - client.deleteNode(nodeToDelete); - }, Runnable::run); + client.deleteNode(nodeToDelete, token); + }, requestsExecutor); }) .collect(Collectors.toList()) .toArray(CompletableFuture[]::new); @@ -249,7 +261,7 @@ public class NodesController extends BaseController { protocol.setUri("ivo://ivoa.net/vospace/core#httpget"); transfer.getProtocols().add(protocol); - return new Job(client.startTransferJob(transfer), Job.JobType.ARCHIVE); + return new Job(client.startTransferJob(transfer, tokenProvider.getToken()), Job.JobType.ARCHIVE); } private String getUri(String path) { @@ -285,6 +297,8 @@ public class NodesController extends BaseController { @PostMapping(value = "/moveOrCopy") public ResponseEntity<List<Job>> moveOrCopyNodes(@RequestBody MoveOrCopyRequest request) throws Exception { + Optional<String> token = tokenProvider.getToken(); + // Creates a transfer request for each copy or move operation CompletableFuture<JobSummary>[] futureJobs = request.getTargets().stream().map(t -> { String target = urlEncodePath(t); @@ -293,7 +307,7 @@ public class NodesController extends BaseController { transfer.setTarget("vos://" + authority + target); transfer.setDirection("vos://" + authority + direction); transfer.setKeepBytes(request.isKeepBytes()); - return CompletableFuture.supplyAsync(() -> client.startTransferJob(transfer), Runnable::run); + return CompletableFuture.supplyAsync(() -> client.startTransferJob(transfer, token), requestsExecutor); }).collect(Collectors.toList()).toArray(CompletableFuture[]::new); // starts all HTTP requests in parallel @@ -321,7 +335,7 @@ public class NodesController extends BaseController { try { // performs polling of job statuses or timeout - CompletableFuture.anyOf(jobsPolling(jobs, cancelled), timeout).join(); + CompletableFuture.anyOf(jobsPolling(jobs, cancelled, token), timeout).join(); } catch (CompletionException ex) { if (ex.getCause() != null && ex.getCause() instanceof VOSpaceException) { throw (VOSpaceException) ex.getCause(); @@ -335,7 +349,7 @@ public class NodesController extends BaseController { .collect(Collectors.toList())); } - private CompletableFuture<?> jobsPolling(List<JobSummary> jobs, AtomicReference<Boolean> cancelled) { + private CompletableFuture<?> jobsPolling(List<JobSummary> jobs, AtomicReference<Boolean> cancelled, Optional<String> token) { return CompletableFuture.runAsync(() -> { List<JobSummary> uncompletedJobs; @@ -350,22 +364,23 @@ public class NodesController extends BaseController { } catch (InterruptedException ex) { break; } - updatePhases(uncompletedJobs); + updatePhases(uncompletedJobs, token); } } while (!uncompletedJobs.isEmpty() && !cancelled.get()); - }, Runnable::run); + }, requestsExecutor); } - private void updatePhases(List<JobSummary> uncompletedJobs) { + private void updatePhases(List<JobSummary> uncompletedJobs, Optional<String> token) { + CompletableFuture[] phaseFutures = uncompletedJobs.stream() .map(job -> CompletableFuture.runAsync(() -> { - ExecutionPhase phase = client.getJobPhase(job.getJobId()); + ExecutionPhase phase = client.getJobPhase(job.getJobId(), token); if (phase == ExecutionPhase.ERROR) { - String errorDetail = client.getErrorDetail(job.getJobId()); + String errorDetail = client.getErrorDetail(job.getJobId(), token); throw new VOSpaceException("Operation failed: " + errorDetail); } job.setPhase(phase); - }, Runnable::run)).collect(Collectors.toList()).toArray(CompletableFuture[]::new); + }, requestsExecutor)).collect(Collectors.toList()).toArray(CompletableFuture[]::new); CompletableFuture.allOf(phaseFutures).join(); } diff --git a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/UploadController.java b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/UploadController.java index 7ddce8eebf291b00147254beac5200bd46fbc5a3..96ab24065321cb09cc139feaed518a6682617451 100644 --- a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/UploadController.java +++ b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/UploadController.java @@ -5,6 +5,7 @@ */ package it.inaf.ia2.vospace.ui.controller; +import it.inaf.ia2.vospace.ui.TokenProvider; import it.inaf.ia2.vospace.ui.client.VOSpaceClient; import it.inaf.ia2.vospace.ui.data.PreUploadResult; import it.inaf.ia2.vospace.ui.data.UploadFilesData; @@ -14,7 +15,9 @@ import it.inaf.ia2.vospace.ui.exception.VOSpaceStatusException; import static it.inaf.oats.vospace.datamodel.NodeUtils.urlEncodePath; import java.util.Arrays; import java.util.List; +import java.util.Optional; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; import java.util.stream.Collectors; import javax.validation.Valid; import net.ivoa.xml.vospace.v2.DataNode; @@ -42,15 +45,23 @@ public class UploadController extends BaseController { @Autowired private VOSpaceClient client; + @Autowired + private TokenProvider tokenProvider; + + @Autowired + private Executor requestsExecutor; + @PostMapping(value = "/preupload", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity<List<PreUploadResult>> prepareForUpload(@RequestBody @Valid UploadFilesData data) { - if (getUser() == null) { + Optional<String> token = tokenProvider.getToken(); + + if (token.isEmpty()) { throw new PermissionDeniedException("File upload not allowed to anonymous users"); } CompletableFuture<PreUploadResult>[] calls - = data.getFiles().stream().map(fileName -> prepareForDownload(getParentPath(data), fileName)) + = data.getFiles().stream().map(fileName -> prepareForDownload(getParentPath(data), fileName, token)) .toArray(CompletableFuture[]::new); List<PreUploadResult> uploadUrls = CompletableFuture.allOf(calls) @@ -69,7 +80,9 @@ public class UploadController extends BaseController { return parentPath; } - public CompletableFuture<PreUploadResult> prepareForDownload(String parentPath, String fileName) { + public CompletableFuture<PreUploadResult> prepareForDownload(String parentPath, String fileName, Optional<String> token) { + + String userName = getUser().getName(); return CompletableFuture.supplyAsync(() -> { @@ -85,7 +98,7 @@ public class UploadController extends BaseController { PreUploadResult result = new PreUploadResult(); try { - createDataNode(nodeUri, getUser().getName()); + createDataNode(nodeUri, userName, token); } catch (Throwable t) { if (t instanceof VOSpaceStatusException && ((VOSpaceStatusException) t).getHttpStatus() == 409) { result.setError("Node already exists"); @@ -97,7 +110,7 @@ public class UploadController extends BaseController { } try { - String uploadUrl = obtainUploadUrl(nodeUri); + String uploadUrl = obtainUploadUrl(nodeUri, token); result.setUrl(uploadUrl); } catch (Throwable t) { if (t instanceof VOSpaceException) { @@ -109,17 +122,17 @@ public class UploadController extends BaseController { try { // attempt to cleanup node metadata - client.deleteNode(path); + client.deleteNode(path, token); } catch (Throwable dt) { LOG.error("Unable to remove node after failed upload URL retrieval", dt); result.setError("Retrieval of upload URL failed. Manual cleanup of node metadata may be necessary"); } } return result; - }, Runnable::run); // Passing current thread Executor to CompletableFuture to avoid "No thread-bound request found" exception + }, requestsExecutor); } - private void createDataNode(String nodeUri, String userId) { + private void createDataNode(String nodeUri, String userId, Optional<String> token) { DataNode node = new DataNode(); node.setUri(nodeUri); @@ -130,10 +143,10 @@ public class UploadController extends BaseController { node.getProperties().add(creator); - client.createNode(node); + client.createNode(node, token); } - private String obtainUploadUrl(String uri) { + private String obtainUploadUrl(String uri, Optional<String> token) { Transfer transfer = new Transfer(); transfer.setDirection("pushToVoSpace"); @@ -143,6 +156,6 @@ public class UploadController extends BaseController { protocol.setUri("ivo://ivoa.net/vospace/core#httpput"); transfer.getProtocols().add(protocol); - return client.getFileServiceEndpoint(transfer); + return client.getFileServiceEndpoint(transfer, token); } } diff --git a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/SharingService.java b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/SharingService.java index 0bfedd6f2d375355e213084d3096761de36e5b8b..37bf7283809e236bf21757a8754b05252eeafddc 100644 --- a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/SharingService.java +++ b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/SharingService.java @@ -12,6 +12,7 @@ import it.inaf.ia2.rap.data.Identity; import it.inaf.ia2.rap.data.IdentityType; import it.inaf.ia2.rap.data.RapUser; import it.inaf.ia2.rap.data.TokenContext; +import it.inaf.ia2.vospace.ui.TokenProvider; import it.inaf.ia2.vospace.ui.client.VOSpaceClient; import it.inaf.ia2.vospace.ui.data.ShareRequest; import it.inaf.ia2.vospace.ui.data.SharingInfo; @@ -64,15 +65,18 @@ public class SharingService { private final GmsClient gmsClient; private final ClientCredentialsRapClient rapClient; private final VOSpaceClient vospaceClient; + private final TokenProvider tokenProvider; private TokenContext tokenContext; private Date lastUpdate; @Autowired - public SharingService(GmsClient gmsClient, ClientCredentialsRapClient gmsRapClient, VOSpaceClient vospaceClient) { + public SharingService(GmsClient gmsClient, ClientCredentialsRapClient gmsRapClient, + VOSpaceClient vospaceClient, TokenProvider tokenProvider) { this.gmsClient = gmsClient; this.rapClient = gmsRapClient; this.vospaceClient = vospaceClient; + this.tokenProvider = tokenProvider; } public SharingInfo getSharingInfo() { @@ -105,11 +109,13 @@ public class SharingService { String groupRead = getGroupString(shareRequest.getGroupRead(), shareRequest.getUserRead()); String groupWrite = getGroupString(shareRequest.getGroupWrite(), shareRequest.getUserWrite()); - Node node = vospaceClient.getNode(shareRequest.getPath()); + Optional<String> token = tokenProvider.getToken(); + + Node node = vospaceClient.getNode(shareRequest.getPath(), token); getNodeProperty(node, NodeProperties.GROUP_READ_URI).setValue(groupRead); getNodeProperty(node, NodeProperties.GROUP_WRITE_URI).setValue(groupWrite); - vospaceClient.setNode(node, shareRequest.isRecursive()); + vospaceClient.setNode(node, shareRequest.isRecursive(), token); if (shareRequest.getNewPeople() != null && !shareRequest.getNewPeople().isEmpty()) { createSharedLinks(shareRequest); diff --git a/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/client/VOSpaceClientTest.java b/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/client/VOSpaceClientTest.java index 899855046b9876a64ecd983a0ddabe5cf9b435fb..a5f6204a7dd5613333a8ac8d34567f9df1700148 100644 --- a/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/client/VOSpaceClientTest.java +++ b/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/client/VOSpaceClientTest.java @@ -5,6 +5,7 @@ */ package it.inaf.ia2.vospace.ui.client; +import it.inaf.ia2.vospace.ui.TokenProvider; import it.inaf.ia2.vospace.ui.exception.BadRequestException; import java.io.ByteArrayInputStream; import java.io.IOException; @@ -17,7 +18,6 @@ import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.util.Optional; import java.util.concurrent.CompletableFuture; -import javax.servlet.http.HttpServletRequest; import net.ivoa.xml.uws.v1.ExecutionPhase; import net.ivoa.xml.vospace.v2.ContainerNode; import net.ivoa.xml.vospace.v2.Protocol; @@ -58,8 +58,6 @@ public class VOSpaceClientTest { staticMock.when(HttpClient::newBuilder).thenReturn(builder); voSpaceClient = new VOSpaceClient("http://localhost/vospace"); } - - voSpaceClient.servletRequest = mock(HttpServletRequest.class); } @Test @@ -69,7 +67,7 @@ public class VOSpaceClientTest { CompletableFuture response = getMockedStreamResponseFuture(200, getResourceFileContent("nodes-response.xml")); when(mockedHttpClient.sendAsync(any(), any())).thenReturn(response); - ContainerNode node = (ContainerNode) voSpaceClient.getNode("/node1"); + ContainerNode node = (ContainerNode) voSpaceClient.getNode("/node1", Optional.empty()); assertEquals("vos://ia2.inaf.it!vospace/node1", node.getUri()); } @@ -87,7 +85,7 @@ public class VOSpaceClientTest { ContainerNode newNode = new ContainerNode(); newNode.setUri("vos://ia2.inaf.it!vospace/mynode/newnode"); - ContainerNode responseNode = (ContainerNode) voSpaceClient.createNode(newNode); + ContainerNode responseNode = (ContainerNode) voSpaceClient.createNode(newNode, Optional.empty()); assertEquals(newNode.getUri(), responseNode.getUri()); } @@ -98,7 +96,7 @@ public class VOSpaceClientTest { newNode.setUri("vos://ia2.inaf.it!vospace/mynode/spaces not encoded"); try { - voSpaceClient.createNode(newNode); + voSpaceClient.createNode(newNode, Optional.empty()); fail("Exception was expected"); } catch (RuntimeException ex) { assertTrue(ex.getCause() instanceof URISyntaxException); @@ -111,7 +109,7 @@ public class VOSpaceClientTest { CompletableFuture response = getMockedStringResponseFuture(200, "error message"); when(mockedHttpClient.sendAsync(any(), any())).thenReturn(response); - assertEquals("error message", voSpaceClient.getErrorDetail("123")); + assertEquals("error message", voSpaceClient.getErrorDetail("123", Optional.empty())); } @Test @@ -128,7 +126,7 @@ public class VOSpaceClientTest { CompletableFuture response = getMockedStreamResponseFuture(200, getResourceFileContent("transfer-response-ok.xml")); when(mockedHttpClient.sendAsync(any(), any())).thenReturn(response); - assertEquals("http://storage1.example.com/trans/mynode", voSpaceClient.getFileServiceEndpoint(transfer)); + assertEquals("http://storage1.example.com/trans/mynode", voSpaceClient.getFileServiceEndpoint(transfer, Optional.empty())); } @Test @@ -155,7 +153,7 @@ public class VOSpaceClientTest { when(mockedHttpClient.sendAsync(any(), any())).thenReturn(response1).thenReturn(response2); BadRequestException ex = assertThrows(BadRequestException.class, () -> { - voSpaceClient.getFileServiceEndpoint(transfer); + voSpaceClient.getFileServiceEndpoint(transfer, Optional.empty()); }); assertEquals("error message", ex.getMessage()); } @@ -184,7 +182,7 @@ public class VOSpaceClientTest { when(mockedHttpClient.sendAsync(any(), any())).thenReturn(response1).thenReturn(response2); BadRequestException ex = assertThrows(BadRequestException.class, () -> { - voSpaceClient.getFileServiceEndpoint(transfer); + voSpaceClient.getFileServiceEndpoint(transfer, Optional.empty()); }); assertEquals("Protocol negotiation failed", ex.getMessage()); } @@ -195,7 +193,7 @@ public class VOSpaceClientTest { CompletableFuture response = getMockedStreamResponseFuture(200, "COMPLETED"); when(mockedHttpClient.sendAsync(any(), any())).thenReturn(response); - assertEquals(ExecutionPhase.COMPLETED, voSpaceClient.getJobPhase("job_id")); + assertEquals(ExecutionPhase.COMPLETED, voSpaceClient.getJobPhase("job_id", Optional.empty())); } @Test @@ -207,7 +205,7 @@ public class VOSpaceClientTest { CompletableFuture response = getMockedStreamResponseFuture(200, getResourceFileContent("node-response.xml")); when(mockedHttpClient.sendAsync(any(), any())).thenReturn(response); - voSpaceClient.setNode(node, true); + voSpaceClient.setNode(node, true, Optional.empty()); // verifying proper URL encoding of + char verify(mockedHttpClient).sendAsync(argThat(req -> { @@ -221,7 +219,7 @@ public class VOSpaceClientTest { CompletableFuture response = getMockedStreamResponseFuture(200, ""); when(mockedHttpClient.sendAsync(any(), any())).thenReturn(response); - voSpaceClient.deleteNode("/not urlencoded"); + voSpaceClient.deleteNode("/not urlencoded", Optional.of("<token>")); } protected static String getResourceFileContent(String fileName) { diff --git a/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/controller/CreateLinksControllerTest.java b/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/controller/CreateLinksControllerTest.java index 92c56f37b89ce9095db770d5d8a58e844be22f56..2a3400411ed6d10c3701eda5391722169f4dddf4 100644 --- a/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/controller/CreateLinksControllerTest.java +++ b/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/controller/CreateLinksControllerTest.java @@ -7,6 +7,7 @@ package it.inaf.ia2.vospace.ui.controller; import com.fasterxml.jackson.databind.ObjectMapper; import it.inaf.ia2.aa.data.User; +import it.inaf.ia2.vospace.ui.TokenProvider; import it.inaf.ia2.vospace.ui.client.VOSpaceClient; import it.inaf.ia2.vospace.ui.data.CreateLinkRequest; import it.inaf.ia2.vospace.ui.exception.VOSpaceStatusException; @@ -16,6 +17,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; import org.mockito.Mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -43,6 +45,9 @@ public class CreateLinksControllerTest { @MockBean private VOSpaceClient client; + @MockBean + private TokenProvider tokenProvider; + @Autowired private MockMvc mockMvc; @@ -59,7 +64,7 @@ public class CreateLinksControllerTest { ContainerNode myFolder = new ContainerNode(); myFolder.setUri("vos://ia2.inaf.it!vospace/path/to/myfolder"); - when(client.getNode("/path/to/myfolder")).thenReturn(myFolder); + when(client.getNode(eq("/path/to/myfolder"), any())).thenReturn(myFolder); CreateLinkRequest request = new CreateLinkRequest(); @@ -78,13 +83,13 @@ public class CreateLinksControllerTest { verify(client, times(1)).createNode(argThat(node -> { return node.getUri().equals("vos://ia2.inaf.it!vospace/path/to/myfolder/myLink"); - })); + }), any()); } @Test public void testUploadLinksNonExistentFolder() throws Exception { - when(client.getNode("/path/to/non-existent")).thenThrow(new VOSpaceStatusException("Not found", 404)); + when(client.getNode(eq("/path/to/non-existent"), any())).thenThrow(new VOSpaceStatusException("Not found", 404)); mockMvc.perform(multipart("/uploadLinks") .file(getListOfLinksMockMultipartFile()) @@ -98,7 +103,7 @@ public class CreateLinksControllerTest { public void testUploadLinks() throws Exception { ContainerNode myFolder = new ContainerNode(); - when(client.getNode("/path/to/myfolder")).thenReturn(myFolder); + when(client.getNode(eq("/path/to/myfolder"), any())).thenReturn(myFolder); mockMvc.perform(multipart("/uploadLinks") .file(getListOfLinksMockMultipartFile()) @@ -107,7 +112,7 @@ public class CreateLinksControllerTest { .andDo(print()) .andExpect(status().isNoContent()); - verify(client, times(75)).createNode(any()); + verify(client, times(75)).createNode(any(), any()); } @Test @@ -133,7 +138,7 @@ public class CreateLinksControllerTest { private void testInvalidContent(String fileContent) throws Exception { ContainerNode myFolder = new ContainerNode(); - when(client.getNode("/path/to/myfolder")).thenReturn(myFolder); + when(client.getNode(eq("/path/to/myfolder"), any())).thenReturn(myFolder); MockMultipartFile file = new MockMultipartFile("file", fileContent.getBytes()); diff --git a/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/controller/DownloadControllerTest.java b/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/controller/DownloadControllerTest.java index 51cd0e737fe7a200af29ca03aa2104ea387fd7fb..31828aa9720d3b7a22f1c367d74636a600465238 100644 --- a/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/controller/DownloadControllerTest.java +++ b/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/controller/DownloadControllerTest.java @@ -7,10 +7,12 @@ package it.inaf.ia2.vospace.ui.controller; import it.inaf.ia2.aa.ServletRapClient; import it.inaf.ia2.aa.data.User; +import it.inaf.ia2.vospace.ui.TokenProvider; import it.inaf.ia2.vospace.ui.client.VOSpaceClient; import static org.junit.jupiter.api.Assertions.assertEquals; import org.junit.jupiter.api.Test; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -35,18 +37,21 @@ public class DownloadControllerTest { @MockBean private ServletRapClient rapClient; + @MockBean + private TokenProvider tokenProvider; + @Autowired private MockMvc mockMvc; @Test public void testDirectDownload() throws Exception { - when(client.getFileServiceEndpoint(any())).thenReturn("http://redirect"); + when(client.getFileServiceEndpoint(any(), any())).thenReturn("http://redirect"); mockMvc.perform(get("/download/myfile")) .andExpect(status().is3xxRedirection()); - verify(client, times(1)).getFileServiceEndpoint(any()); + verify(client, times(1)).getFileServiceEndpoint(any(), any()); } @Test @@ -55,7 +60,7 @@ public class DownloadControllerTest { User user = mock(User.class); when(user.getAccessToken()).thenReturn("<token>"); - when(client.getArchiveJobHref("job123")).thenReturn("http://file-service/job123.zip"); + when(client.getArchiveJobHref(eq("job123"), any())).thenReturn("http://file-service/job123.zip"); when(rapClient.exchangeToken(any(), any())).thenReturn("<new-token>"); diff --git a/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/controller/JobControllerTest.java b/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/controller/JobControllerTest.java index 00b1a262458a78e7a660c658d7b96d9cc863c79f..a3ce64531e127a4bad292783e5b59299ea51badc 100644 --- a/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/controller/JobControllerTest.java +++ b/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/controller/JobControllerTest.java @@ -53,7 +53,7 @@ public class JobControllerTest { JobSummary job = new JobSummary(); job.setPhase(ExecutionPhase.QUEUED); - when(client.startTransferJob(any())).thenReturn(job); + when(client.startTransferJob(any(), any())).thenReturn(job); mockMvc.perform(post("/recall") .contentType(MediaType.APPLICATION_JSON) @@ -73,9 +73,9 @@ public class JobControllerTest { assertEquals("file1", transfer.getView().getParam().get(0).getValue()); assertEquals("file2", transfer.getView().getParam().get(1).getValue()); return true; - }))).thenReturn(job); + }), any())).thenReturn(job); - when(client.getFileServiceEndpoint(any())).thenReturn("http://file-service/path/to/file"); + when(client.getFileServiceEndpoint(any(), any())).thenReturn("http://file-service/path/to/file"); mockMvc.perform(post("/recall") .contentType(MediaType.APPLICATION_JSON) @@ -89,7 +89,7 @@ public class JobControllerTest { JobSummary job = new JobSummary(); job.setPhase(ExecutionPhase.ERROR); - when(client.getErrorDetail(any())).thenReturn("Error was xxx"); + when(client.getErrorDetail(any(), any())).thenReturn("Error was xxx"); ErrorSummary errorSummary = new ErrorSummary(); errorSummary.setHasDetail(true); @@ -97,7 +97,7 @@ public class JobControllerTest { job.setErrorSummary(errorSummary); - when(client.startTransferJob(any())).thenReturn(job); + when(client.startTransferJob(any(), any())).thenReturn(job); testErrorCall(ex -> assertTrue(ex.getMessage().contains("Error was xxx"))); } @@ -114,7 +114,7 @@ public class JobControllerTest { job.setErrorSummary(errorSummary); - when(client.startTransferJob(any())).thenReturn(job); + when(client.startTransferJob(any(), any())).thenReturn(job); testErrorCall(ex -> assertTrue(ex.getMessage().contains("simple message") && ex.getMessage().contains("retry"))); } @@ -125,7 +125,7 @@ public class JobControllerTest { JobSummary job = new JobSummary(); job.setPhase(ExecutionPhase.ERROR); - when(client.startTransferJob(any())).thenReturn(job); + when(client.startTransferJob(any(), any())).thenReturn(job); testErrorCall(ex -> assertTrue(ex.getMessage().contains("error"))); } @@ -133,8 +133,8 @@ public class JobControllerTest { @Test public void testGetJobs() throws Exception { - when(client.getAsyncRecallJobs()).thenReturn(List.of(new Job(new JobSummary(), Job.JobType.ASYNC_RECALL))); - when(client.getArchiveJobs()).thenReturn(List.of(new Job(new JobSummary(), Job.JobType.ARCHIVE))); + when(client.getAsyncRecallJobs(any())).thenReturn(List.of(new Job(new JobSummary(), Job.JobType.ASYNC_RECALL))); + when(client.getArchiveJobs(any())).thenReturn(List.of(new Job(new JobSummary(), Job.JobType.ARCHIVE))); mockMvc.perform(get("/jobs")) .andExpect(status().isOk()) diff --git a/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/controller/NodesControllerTest.java b/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/controller/NodesControllerTest.java index 14c1535cd28eba1f67c75f8c81a17f1a0e3de871..02a43f0eb0ba327bfd2aea3cf50212afc780073f 100644 --- a/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/controller/NodesControllerTest.java +++ b/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/controller/NodesControllerTest.java @@ -7,6 +7,7 @@ package it.inaf.ia2.vospace.ui.controller; import com.fasterxml.jackson.databind.ObjectMapper; import it.inaf.ia2.aa.data.User; +import it.inaf.ia2.vospace.ui.TokenProvider; import it.inaf.ia2.vospace.ui.client.VOSpaceClient; import it.inaf.ia2.vospace.ui.data.MoveOrCopyRequest; import it.inaf.ia2.vospace.ui.exception.VOSpaceException; @@ -47,7 +48,7 @@ import org.springframework.web.util.NestedServletException; @SpringBootTest @AutoConfigureMockMvc -@TestPropertySource(properties = {"vospace-authority=example.com!vospace", "pollingTimeout=2"}) +@TestPropertySource(properties = {"vospace-authority=example.com!vospace", "pollingTimeout=3"}) public class NodesControllerTest { private static final ObjectMapper MAPPER = new ObjectMapper(); @@ -55,6 +56,9 @@ public class NodesControllerTest { @MockBean private VOSpaceClient client; + @MockBean + private TokenProvider tokenProvider; + @Autowired private NodesController nodesController; @@ -64,34 +68,34 @@ public class NodesControllerTest { @Test public void testListNodesEmpty() throws Exception { - when(client.getNode(any())).thenReturn(new DataNode()); + when(client.getNode(any(), any())).thenReturn(new DataNode()); mockMvc.perform(get("/nodes")) .andExpect(status().isOk()); - verify(client, times(1)).getNode(eq("/")); + verify(client, times(1)).getNode(eq("/"), any()); } @Test public void testListNodesRoot() throws Exception { - when(client.getNode(any())).thenReturn(new DataNode()); + when(client.getNode(any(), any())).thenReturn(new DataNode()); mockMvc.perform(get("/nodes/")) .andExpect(status().isOk()); - verify(client, times(1)).getNode(eq("/")); + verify(client, times(1)).getNode(eq("/"), any()); } @Test public void testListNodesComplexPath() throws Exception { - when(client.getNode(any())).thenReturn(new DataNode()); + when(client.getNode(any(), any())).thenReturn(new DataNode()); mockMvc.perform(get("/nodes/a/b/c")) .andExpect(status().isOk()); - verify(client, times(1)).getNode(eq("/a/b/c")); + verify(client, times(1)).getNode(eq("/a/b/c"), any()); } @Test @@ -104,15 +108,15 @@ public class NodesControllerTest { .content(MAPPER.writeValueAsString(paths))) .andExpect(status().isNoContent()); - verify(client, times(1)).deleteNode(eq("/a/b/c")); - verify(client, times(1)).deleteNode(eq("/e/f/g")); + verify(client, times(1)).deleteNode(eq("/a/b/c"), any()); + verify(client, times(1)).deleteNode(eq("/e/f/g"), any()); } @Test public void testErrorOnDelete() throws Exception { doThrow(new RuntimeException()) - .when(client).deleteNode(any()); + .when(client).deleteNode(any(), any()); boolean exception = false; try { @@ -164,9 +168,9 @@ public class NodesControllerTest { child7.setTarget("http://external-link"); parent.getNodes().add(child7); - when(client.getNode(eq("/a/b/c"))).thenReturn(parent); - when(client.getNode(eq("/a/b/c/c1"))).thenReturn(child1); - when(client.getNode(eq("/a/b/c/c2"))).thenReturn(child2); + when(client.getNode(eq("/a/b/c"), any())).thenReturn(parent); + when(client.getNode(eq("/a/b/c/c1"), any())).thenReturn(child1); + when(client.getNode(eq("/a/b/c/c2"), any())).thenReturn(child2); String response = mockMvc.perform(get("/nodesForMoveOrCopy") .param("path", "/a/b/c") @@ -183,13 +187,13 @@ public class NodesControllerTest { assertTrue(response.contains("c6")); // link to container assertFalse(response.contains("c7")); // external link - verify(client, times(1)).getNode(eq("/a/b/c")); + verify(client, times(1)).getNode(eq("/a/b/c"), any()); } @Test public void testMoveNodeSuccess() throws Exception { - when(client.getJobPhase("job_id")) + when(client.getJobPhase(eq("job_id"), any())) .thenReturn(ExecutionPhase.EXECUTING) .thenReturn(ExecutionPhase.COMPLETED); @@ -201,7 +205,7 @@ public class NodesControllerTest { @Test public void testMoveNodeExecuting() throws Exception { - when(client.getJobPhase("job_id")) + when(client.getJobPhase(eq("job_id"), any())) .thenReturn(ExecutionPhase.EXECUTING); testMoveNode() @@ -212,10 +216,10 @@ public class NodesControllerTest { @Test public void testMoveNodeError() throws Exception { - when(client.getJobPhase("job_id")) + when(client.getJobPhase(eq("job_id"), any())) .thenReturn(ExecutionPhase.ERROR); - when(client.getErrorDetail("job_id")).thenReturn("move_error"); + when(client.getErrorDetail(eq("job_id"), any())).thenReturn("move_error"); try { testMoveNode(); @@ -239,7 +243,7 @@ public class NodesControllerTest { .content("{\"parentPath\": \"/parent\", \"name\": \"newFolder\"}")) .andExpect(status().is2xxSuccessful()); - verify(client, times(1)).createNode(any()); + verify(client, times(1)).createNode(any(), any()); } @Test @@ -261,13 +265,13 @@ public class NodesControllerTest { DataNode linkedNode = new DataNode(); linkedNode.setUri("vos://example.com!vospace/myfile"); - when(client.getNode(eq("/"))).thenReturn(parent); - when(client.getNode(eq("/myfile"))).thenReturn(linkedNode); + when(client.getNode(eq("/"), any())).thenReturn(parent); + when(client.getNode(eq("/myfile"), any())).thenReturn(linkedNode); mockMvc.perform(get("/nodes")) .andExpect(status().isOk()); - verify(client, times(1)).getNode(eq("/")); + verify(client, times(1)).getNode(eq("/"), any()); } private ResultActions testMoveNode() throws Exception { @@ -275,7 +279,7 @@ public class NodesControllerTest { JobSummary job = new JobSummary(); job.setJobId("job_id"); - when(client.startTransferJob(any())).thenReturn(job); + when(client.startTransferJob(any(), any())).thenReturn(job); MoveOrCopyRequest request = new MoveOrCopyRequest(); request.setTargets(List.of("/path/to/target")); diff --git a/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/controller/UploadControllerTest.java b/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/controller/UploadControllerTest.java index 31de6d21f785fa8cd470a03c377b263a994f78fd..5928b82334f2425ee44279f4cf6da3605e864bce 100644 --- a/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/controller/UploadControllerTest.java +++ b/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/controller/UploadControllerTest.java @@ -7,11 +7,13 @@ package it.inaf.ia2.vospace.ui.controller; import com.fasterxml.jackson.databind.ObjectMapper; import it.inaf.ia2.aa.data.User; +import it.inaf.ia2.vospace.ui.TokenProvider; import it.inaf.ia2.vospace.ui.client.VOSpaceClient; import it.inaf.ia2.vospace.ui.data.UploadFilesData; import it.inaf.ia2.vospace.ui.exception.VOSpaceException; import it.inaf.ia2.vospace.ui.exception.VOSpaceStatusException; import java.util.List; +import java.util.Optional; import static org.hamcrest.core.IsNull.nullValue; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -44,6 +46,9 @@ public class UploadControllerTest { @MockBean private VOSpaceClient client; + @MockBean + private TokenProvider tokenProvider; + @Autowired private MockMvc mockMvc; @@ -53,6 +58,7 @@ public class UploadControllerTest { @BeforeEach public void setUp() { when(user.getName()).thenReturn("user_id"); + when(tokenProvider.getToken()).thenReturn(Optional.of("<token>")); } @Test @@ -62,7 +68,7 @@ public class UploadControllerTest { data.setParentPath("/mynode"); data.setFiles(List.of("test.txt")); - when(client.getFileServiceEndpoint(any())).thenReturn("http://files/mynode/test.txt"); + when(client.getFileServiceEndpoint(any(), any())).thenReturn("http://files/mynode/test.txt"); mockMvc.perform(post("/preupload") .sessionAttr("user_data", user) @@ -81,7 +87,7 @@ public class UploadControllerTest { data.setParentPath("/"); data.setFiles(List.of("test.txt")); - when(client.getFileServiceEndpoint(any())).thenReturn("http://files/test.txt"); + when(client.getFileServiceEndpoint(any(), any())).thenReturn("http://files/test.txt"); mockMvc.perform(post("/preupload") .sessionAttr("user_data", user) @@ -96,6 +102,8 @@ public class UploadControllerTest { @Test public void testUploadNotAllowedToAnonymous() throws Exception { + when(tokenProvider.getToken()).thenReturn(Optional.empty()); + UploadFilesData data = new UploadFilesData(); data.setParentPath("/"); data.setFiles(List.of("test.txt")); @@ -113,7 +121,7 @@ public class UploadControllerTest { data.setParentPath("/mynode"); data.setFiles(List.of("test.txt")); - doThrow(new VOSpaceStatusException("Conflict", 409)).when(client).createNode(any()); + doThrow(new VOSpaceStatusException("Conflict", 409)).when(client).createNode(any(), any()); mockMvc.perform(post("/preupload") .sessionAttr("user_data", user) @@ -132,7 +140,7 @@ public class UploadControllerTest { data.setParentPath("/mynode"); data.setFiles(List.of("test.txt")); - doThrow(new VOSpaceStatusException("Server Error", 500)).when(client).createNode(any()); + doThrow(new VOSpaceStatusException("Server Error", 500)).when(client).createNode(any(), any()); mockMvc.perform(post("/preupload") .sessionAttr("user_data", user) @@ -151,7 +159,7 @@ public class UploadControllerTest { data.setParentPath("/mynode"); data.setFiles(List.of("test.txt")); - doThrow(new VOSpaceException("Unable to connect")).when(client).getFileServiceEndpoint(any()); + doThrow(new VOSpaceException("Unable to connect")).when(client).getFileServiceEndpoint(any(), any()); mockMvc.perform(post("/preupload") .sessionAttr("user_data", user) @@ -162,7 +170,7 @@ public class UploadControllerTest { .andExpect(jsonPath("$[0].error").value("Unable to connect")) .andExpect(jsonPath("$[0].url").value(nullValue())); - verify(client, times(1)).deleteNode(eq("/mynode/test.txt")); + verify(client, times(1)).deleteNode(eq("/mynode/test.txt"), any()); } @Test @@ -172,7 +180,7 @@ public class UploadControllerTest { data.setParentPath("/mynode"); data.setFiles(List.of("test.txt")); - doThrow(new NullPointerException()).when(client).getFileServiceEndpoint(any()); + doThrow(new NullPointerException()).when(client).getFileServiceEndpoint(any(), any()); mockMvc.perform(post("/preupload") .sessionAttr("user_data", user) @@ -183,7 +191,7 @@ public class UploadControllerTest { .andExpect(jsonPath("$[0].error").value("Unable to obtain upload URL")) .andExpect(jsonPath("$[0].url").value(nullValue())); - verify(client, times(1)).deleteNode(eq("/mynode/test.txt")); + verify(client, times(1)).deleteNode(eq("/mynode/test.txt"), any()); } @Test @@ -193,8 +201,8 @@ public class UploadControllerTest { data.setParentPath("/mynode"); data.setFiles(List.of("test.txt")); - doThrow(new NullPointerException()).when(client).getFileServiceEndpoint(any()); - doThrow(new VOSpaceException("error")).when(client).deleteNode(any()); + doThrow(new NullPointerException()).when(client).getFileServiceEndpoint(any(), any()); + doThrow(new VOSpaceException("error")).when(client).deleteNode(any(), any()); mockMvc.perform(post("/preupload") .sessionAttr("user_data", user) @@ -205,6 +213,6 @@ public class UploadControllerTest { .andExpect(jsonPath("$[0].error").value("Retrieval of upload URL failed. Manual cleanup of node metadata may be necessary")) .andExpect(jsonPath("$[0].url").value(nullValue())); - verify(client, times(1)).deleteNode(eq("/mynode/test.txt")); + verify(client, times(1)).deleteNode(eq("/mynode/test.txt"), any()); } } diff --git a/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/service/SharingServiceTest.java b/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/service/SharingServiceTest.java index df918a0e005288fd7f17e3a78adb2c66fda971c9..fc99091bbb08e8834c8e6d03d3ff15f836c96a37 100644 --- a/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/service/SharingServiceTest.java +++ b/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/service/SharingServiceTest.java @@ -11,6 +11,7 @@ import it.inaf.ia2.rap.data.AccessTokenResponse; import it.inaf.ia2.rap.data.Identity; import it.inaf.ia2.rap.data.IdentityType; import it.inaf.ia2.rap.data.RapUser; +import it.inaf.ia2.vospace.ui.TokenProvider; import it.inaf.ia2.vospace.ui.client.VOSpaceClient; import it.inaf.ia2.vospace.ui.data.ShareRequest; import it.inaf.ia2.vospace.ui.data.SharingInfo; @@ -59,6 +60,9 @@ public class SharingServiceTest { @Mock private VOSpaceClient vospaceClient; + @Mock + private TokenProvider tokenProvider; + @InjectMocks private SharingService sharingService; @@ -104,7 +108,7 @@ public class SharingServiceTest { groupReadProperty.setValue("group1"); node.getProperties().add(groupReadProperty); - when(vospaceClient.getNode(any())).thenReturn(node); + when(vospaceClient.getNode(any(), any())).thenReturn(node); ShareRequest shareRequest = new ShareRequest(); shareRequest.setPath("/mynode"); @@ -130,7 +134,7 @@ public class SharingServiceTest { assertTrue(groupWrite.contains("people.bianca\\.verdi")); assertTrue(groupWrite.contains("group2")); return true; - }), anyBoolean()); + }), anyBoolean(), any()); } @Test @@ -139,7 +143,7 @@ public class SharingServiceTest { DataNode node = new DataNode(); node.setUri("vos://example.com!vospace/anna.bianchi/mynode"); - when(vospaceClient.getNode(any())).thenReturn(node); + when(vospaceClient.getNode(any(), any())).thenReturn(node); ShareRequest shareRequest = new ShareRequest(); shareRequest.setPath("/anna.bianchi/mynode");