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.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.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.uws.v1.ResultReference; import net.ivoa.xml.vospace.v2.DataNode; 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; @Autowired private NodeDAO nodeDao; @Autowired private LocationDAO locationDAO; @Autowired private HttpServletRequest servletRequest; @Autowired private ServletRapClient rapClient; @Autowired private CreateNodeController createNodeController; public void setTransferJobResult(JobSummary job, Transfer transfer) { List results = new ArrayList<>(); ResultReference result = new ResultReference(); result.setHref(getEndpoint(job, transfer)); results.add(result); job.setResults(results); // Moved phase setting to caller method for ERROR management } /** * Sets the endpoint value for all valid protocols (protocol negotiation). */ public void setSyncTransferEndpoints(JobSummary job) { Transfer transfer = getTransfer(job); 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: 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() .filter(protocol -> validProtocolUris.contains(protocol.getUri())) .collect(Collectors.toList()); if (validProtocols.isEmpty()) { Protocol protocol = transfer.getProtocols().get(0); throw new ProtocolNotSupportedException(protocol.getUri()); } String endpoint = getEndpoint(job, transfer); validProtocols.stream().forEach(p -> p.setEndpoint(endpoint)); // Returns modified transfer containing only valid protocols transfer.getProtocols().clear(); transfer.getProtocols().addAll(validProtocols); } private Node getEndpointNode(String relativePath, JobService.JobDirection jobType, User user) { Optional optNode = nodeDao.listNode(relativePath); if (optNode.isPresent()) { return optNode.get(); } else { switch (jobType) { case pullFromVoSpace: throw new NodeNotFoundException(relativePath); case pushToVoSpace: case pullToVoSpace: DataNode newNode = new DataNode(); newNode.setUri(relativePath); return createNodeController.createNode(newNode, user); default: throw new InternalFaultException("No supported job direction specified"); } } } private String getEndpoint(JobSummary job, Transfer transfer) { String relativePath = transfer.getTarget().substring("vos://".length() + authority.length()); 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 new PermissionDeniedException(relativePath); } break; case pullFromVoSpace: if (!NodeUtils.checkIfReadable(node, creator, groups)) { throw new PermissionDeniedException(relativePath); } break; default: throw new InternalFaultException("No supported job direction specified"); } if (NodeUtils.getIsBusy(node)) { throw new NodeBusyException(relativePath); } Location location = locationDAO.getNodeLocation(relativePath).orElse(null); String endpoint; 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 (!"true".equals(NodeProperties.getNodePropertyByURI(node, NodeProperties.PUBLIC_READ_URI))) { endpoint += "&token=" + getEndpointToken(fileServiceUrl + relativePath); } return endpoint; } 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 = nodeUri.replaceAll("vos://[^/]+", ""); 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); } }