Newer
Older
/*
* This file is part of vospace-rest
* Copyright (C) 2021 Istituto Nazionale di Astrofisica
* SPDX-License-Identifier: GPL-3.0-or-later
*/
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;
Nicola Fulvio Calabria
committed
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;
Sonia Zorba
committed
import it.inaf.oats.vospace.exception.InvalidArgumentException;
import it.inaf.oats.vospace.exception.NodeNotFoundException;
import it.inaf.oats.vospace.exception.PermissionDeniedException;
Nicola Fulvio Calabria
committed
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.Objects;
Nicola Fulvio Calabria
committed
import java.util.Optional;
Sonia Zorba
committed
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletRequest;
Nicola Fulvio Calabria
committed
import net.ivoa.xml.vospace.v2.DataNode;
import net.ivoa.xml.vospace.v2.LinkNode;
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;
Nicola Fulvio Calabria
committed
@Autowired
private LocationDAO locationDAO;
@Autowired
private LinkedServiceDAO linkedServiceDAO;
@Autowired
private HttpServletRequest servletRequest;
@Autowired
private ServletRapClient rapClient;
Nicola Fulvio Calabria
committed
@Autowired
private CreateNodeService createNodeService;
Nicola Fulvio Calabria
committed
@Autowired
private FileServiceClient fileServiceClient;
Sonia Zorba
committed
/**
Sonia Zorba
committed
* For a given job, returns a new transfer object containing only valid
* protocols (protocol negotiation) and sets proper endpoints on them.
Sonia Zorba
committed
*/
Sonia Zorba
committed
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
Sonia Zorba
committed
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);
Sonia Zorba
committed
List<ProtocolType> validProtocolTypes = new ArrayList<>();
switch (jobDirection) {
Sonia Zorba
committed
case pullFromVoSpace:
validProtocolTypes.add(ProtocolType.HTTPSGET);
Sonia Zorba
committed
case pullToVoSpace:
validProtocolTypes.add(ProtocolType.HTTPGET);
Sonia Zorba
committed
break;
case pushToVoSpace:
validProtocolTypes.add(ProtocolType.HTTPPUT);
Sonia Zorba
committed
break;
default:
throw new InternalFaultException("Unsupported job direction specified");
Sonia Zorba
committed
}
List<Protocol> validProtocols
= transfer.getProtocols().stream()
// discard invalid protocols by uri String
.filter(protocol
-> validProtocolTypes.stream().map(pt
-> {
return pt.getUri();
})
.collect(Collectors.toList())
.contains(protocol.getUri()))
Sonia Zorba
committed
.map(p -> {
// set endpoints
String endpoint = getEndpoint(job, transfer);
ProtocolType pt
= ProtocolType.getProtocolTypeFromURI(p.getUri());
if (pt.isEndpointCompliant(endpoint)) {
Protocol protocol = new Protocol();
protocol.setUri(p.getUri());
protocol.setEndpoint(endpoint);
return protocol;
} else {
return null;
}
}).filter(Objects::nonNull)
.collect(Collectors.toList());
Sonia Zorba
committed
if (validProtocols.isEmpty()) {
Protocol protocol = transfer.getProtocols().get(0);
Nicola Fulvio Calabria
committed
throw new ProtocolNotSupportedException(protocol.getUri());
Sonia Zorba
committed
Sonia Zorba
committed
negotiatedTransfer.getProtocols().addAll(validProtocols);
Sonia Zorba
committed
return negotiatedTransfer;
Nicola Fulvio Calabria
committed
private Node getEndpointNode(String relativePath,
JobService.JobDirection jobType,
Nicola Fulvio Calabria
committed
User user) {
Optional<Node> optNode = nodeDao.listNode(relativePath);
Sonia Zorba
committed
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;
Nicola Fulvio Calabria
committed
} 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);
Nicola Fulvio Calabria
committed
default:
throw new InternalFaultException("No supported job direction specified");
}
}
}
private String getEndpoint(JobSummary job, Transfer transfer) {
Sonia Zorba
committed
String relativePath = URIUtils.returnVosPathFromNodeURI(transfer.getTarget(), authority);
Nicola Fulvio Calabria
committed
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);
Nicola Fulvio Calabria
committed
Node node = this.getEndpointNode(relativePath, jobType, user);
Nicola Fulvio Calabria
committed
switch (jobType) {
case pushToVoSpace:
case pullToVoSpace:
if (!NodeUtils.checkIfWritable(node, creator, groups)) {
throw PermissionDeniedException.forPath(relativePath);
Nicola Fulvio Calabria
committed
}
break;
case pullFromVoSpace:
// Refresh relative path: it can differ in case of links followed
relativePath = NodeUtils.getVosPath(node);
Nicola Fulvio Calabria
committed
if (!NodeUtils.checkIfReadable(node, creator, groups)) {
throw PermissionDeniedException.forPath(relativePath);
Nicola Fulvio Calabria
committed
}
break;
default:
Nicola Fulvio Calabria
committed
throw new InternalFaultException("No supported job direction specified");
Nicola Fulvio Calabria
committed
}
if (NodeUtils.getIsBusy(node)) {
throw new NodeBusyException(relativePath);
}
Sonia Zorba
committed
if (isArchiveView(transfer)) {
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()) {
Sonia Zorba
committed
throw new InternalFaultException("Empty job payload for job " + job.getJobId());
}
if (jobPayload.size() > 1) {
Sonia Zorba
committed
throw new InternalFaultException("Multiple objects in job payload not supported");
}
if (!(jobPayload.get(0) instanceof Transfer)) {
Sonia Zorba
committed
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;
}
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
public enum ProtocolType {
// Please keep the URIs in this enum UNIQUE!
// will add a unit test to check this
HTTPGET("ivo://ivoa.net/vospace/core#httpget", "http"),
HTTPSGET("ivo://ivoa.net/vospace/core#httpsget", "https"),
HTTPPUT("ivo://ivoa.net/vospace/core#httpput", "http"),
HTTPSPUT("ivo://ivoa.net/vospace/core#httpsput", "https");
private final String uri;
private final String protocolString;
private ProtocolType(String uri, String protocolString) {
this.uri = uri;
this.protocolString = protocolString;
}
public String getUri() {
return this.uri;
}
public boolean isEndpointCompliant(String endpoint) {
return endpoint.toLowerCase()
.startsWith(this.protocolString + "://");
}
public static ProtocolType getProtocolTypeFromURI(String uri) {
for (ProtocolType pt : ProtocolType.values()) {
if (pt.getUri().equals(uri)) {
return pt;
}
}
return null;
}
}