/* * 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.ServletRapClient; import it.inaf.ia2.aa.data.User; import it.inaf.ia2.rap.client.call.TokenExchangeRequest; import it.inaf.oats.vospace.JobService.JobDirection; import it.inaf.oats.vospace.datamodel.NodeProperties; import it.inaf.oats.vospace.datamodel.NodeUtils; import static it.inaf.oats.vospace.datamodel.NodeUtils.urlEncodePath; 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.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.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.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; 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.DataNode; import net.ivoa.xml.vospace.v2.LinkNode; import net.ivoa.xml.vospace.v2.Node; import net.ivoa.xml.vospace.v2.Protocol; import net.ivoa.xml.vospace.v2.Transfer; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; @Service public class UriService { @Value("${vospace-authority}") private String authority; @Value("${file-service-url}") private String fileServiceUrl; @Value("${link-max-depth}") private int linkMaxDepth; @Autowired private NodeDAO nodeDao; @Autowired private LocationDAO locationDAO; @Autowired private LinkedServiceDAO linkedServiceDAO; @Autowired private HttpServletRequest servletRequest; @Autowired private ServletRapClient rapClient; @Autowired private CreateNodeService createNodeService; @Autowired private FileServiceClient fileServiceClient; /** * For a given job, returns a new transfer object containing only valid * protocols (protocol negotiation) and sets proper endpoints on them. */ public Transfer getNegotiatedTransfer(JobSummary job, Transfer transfer) { // Original transfer object shouldn't be modified, so a new transfer object is created Transfer negotiatedTransfer = new Transfer(); negotiatedTransfer.setTarget(transfer.getTarget()); negotiatedTransfer.setDirection(transfer.getDirection()); // according to examples found in specification view is not copied if (transfer.getProtocols().isEmpty()) { // At least one protocol is expected from client throw new InvalidArgumentException("Transfer contains no protocols"); } JobService.JobDirection jobDirection = JobDirection.getJobDirectionEnumFromTransfer(transfer); List validProtocolUris = new ArrayList<>(); switch (jobDirection) { case pullFromVoSpace: case pullToVoSpace: validProtocolUris.add("ivo://ivoa.net/vospace/core#httpget"); break; case pushToVoSpace: validProtocolUris.add("ivo://ivoa.net/vospace/core#httpput"); break; default: throw new InternalFaultException("Unsupported job direction specified"); } List validProtocols = transfer.getProtocols().stream() // discard invalid protocols .filter(protocol -> validProtocolUris.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()); if (validProtocols.isEmpty()) { Protocol protocol = transfer.getProtocols().get(0); throw new ProtocolNotSupportedException(protocol.getUri()); } negotiatedTransfer.getProtocols().addAll(validProtocols); return negotiatedTransfer; } private Node getEndpointNode(String relativePath, JobService.JobDirection jobType, User user) { Optional optNode = nodeDao.listNode(relativePath); if (optNode.isPresent()) { Node node = optNode.get(); if (jobType.equals(JobService.JobDirection.pullFromVoSpace)) { if (node instanceof LinkNode) { if (!NodeUtils.checkIfReadable(node, user.getName(), user.getGroups())) { throw PermissionDeniedException.forPath(relativePath); } node = this.followLink((LinkNode) node); } } return node; } else { switch (jobType) { case pullFromVoSpace: throw new NodeNotFoundException(relativePath); case pushToVoSpace: case pullToVoSpace: DataNode newNode = new DataNode(); newNode.setUri(URIUtils.returnURIFromVosPath(relativePath, authority)); return createNodeService.createNode(newNode, relativePath, user); default: throw new InternalFaultException("No supported job direction specified"); } } } private String getEndpoint(JobSummary job, Transfer transfer) { String relativePath = URIUtils.returnVosPathFromNodeURI(transfer.getTarget(), authority); User user = (User) servletRequest.getUserPrincipal(); String creator = user.getName(); List groups = user.getGroups(); // Check privileges write or read according to job type JobService.JobDirection jobType = JobDirection.getJobDirectionEnumFromTransfer(transfer); Node node = this.getEndpointNode(relativePath, jobType, user); switch (jobType) { case pushToVoSpace: case pullToVoSpace: if (!NodeUtils.checkIfWritable(node, creator, groups)) { throw PermissionDeniedException.forPath(relativePath); } break; case pullFromVoSpace: // 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); } break; default: throw new InternalFaultException("No supported job direction specified"); } if (NodeUtils.getIsBusy(node)) { throw new NodeBusyException(relativePath); } if (isArchiveView(transfer)) { return fileServiceClient.startArchiveJob(transfer, job.getJobId()); } boolean isLinkNode = node instanceof LinkNode; String endpoint; if (isLinkNode) { endpoint = ((LinkNode) node).getTarget(); } else { Location location = locationDAO.getNodeLocation(relativePath).orElse(null); 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 (isLinkNode) { String linkTarget = ((LinkNode) node).getTarget(); if (linkedServiceDAO.isLinkedServiceUrl(linkTarget)) { endpoint += "&token=" + getEndpointToken(linkTarget); } } else if (!"true".equals(NodeProperties.getNodePropertyByURI(node, NodeProperties.PUBLIC_READ_URI))) { endpoint += "&token=" + getEndpointToken(fileServiceUrl + relativePath); } return endpoint; } private boolean isArchiveView(Transfer transfer) { if (transfer.getView() == null) { return false; } String viewUri = transfer.getView().getUri(); return Views.TAR_VIEW_URI.equals(viewUri) || Views.ZIP_VIEW_URI.equals(viewUri); } private String getEndpointToken(String endpoint) { String token = ((User) servletRequest.getUserPrincipal()).getAccessToken(); 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); } public void setNodeRemoteLocation(String nodeUri, String contentUri) { URL url; try { url = new URL(contentUri); } catch (MalformedURLException ex) { throw new InternalFaultException(ex); } Location location = locationDAO.findPortalLocation(url.getHost()).orElseThrow(() -> new InternalFaultException("No registered location found for host " + url.getHost())); String vosPath = URIUtils.returnVosPathFromNodeURI(nodeUri, authority); String fileName = url.getPath().substring(url.getPath().lastIndexOf("/") + 1); nodeDao.setNodeLocation(vosPath, location.getId(), fileName); } public Transfer getTransfer(JobSummary job) { List jobPayload = job.getJobInfo().getAny(); if (jobPayload.isEmpty()) { throw new InternalFaultException("Empty job payload for job " + job.getJobId()); } if (jobPayload.size() > 1) { throw new InternalFaultException("Multiple objects in job payload not supported"); } if (!(jobPayload.get(0) instanceof Transfer)) { throw new InternalFaultException(jobPayload.get(0).getClass().getCanonicalName() + " not supported as job payload. Job id: " + job.getJobId()); } return (Transfer) job.getJobInfo().getAny().get(0); } private 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)) { return linkNode; } else { String targetPath = URIUtils.returnVosPathFromNodeURI(linkTarget, authority); Optional 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; } } } }