Skip to content
UriService.java 12 KiB
Newer Older
Sonia Zorba's avatar
Sonia Zorba committed
/*
 * 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 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;

    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);
                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<Protocol> validProtocols
                = transfer.getProtocols().stream()
                        .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);
    private Node getEndpointNode(String relativePath,
            User user) {
        Optional<Node> optNode = nodeDao.listNode(relativePath);
            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<String> 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);
                // 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);
                throw new InternalFaultException("No supported job direction specified");
        }

        if (NodeUtils.getIsBusy(node)) {
            throw new NodeBusyException(relativePath);
        }
            return fileServiceClient.startArchiveJob(transfer, job.getJobId());
        }

        boolean isLinkNode = node instanceof LinkNode;
        if (isLinkNode) {
            endpoint = ((LinkNode) node).getTarget();

            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<Object> 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<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;
            }