Commit 6af38eda authored by Sonia Zorba's avatar Sonia Zorba
Browse files

Handled token for private files retrieval. Added several tests

parent 7681799a
Pipeline #843 passed with stages
in 1 minute and 6 seconds
......@@ -2,6 +2,31 @@
## Database
This VOSpace implementation uses the database populated by the [VOSpace Transfer Service application](https://www.ict.inaf.it/gitlab/ia2/vospace-transfer-service). To avoid duplicating database definitions, DAO test classes load the database directly from the files of that repository. We assume that when running the tests the git repository exists and it is located in the same parent folder containing this repository. We could decide to create a dedicate common repository for sharing only the database structure and configuration files between the 2 projects.
This VOSpace implementation uses the database populated by the [VOSpace Transfer Service application](https://www.ict.inaf.it/gitlab/ia2/vospace-transfer-service). The structure of the database is defined in a [separate repository](https://www.ict.inaf.it/gitlab/ia2/vospace-file-catalog). To avoid duplicating database definitions, DAO test classes load the database directly from the files of that repository. We assume that when running the tests the git repository exists and it is located in the same parent folder containing this repository.
To reconfigure the path of that repository edit the property `init_database_scripts_path` in test.properties.
## Loading fake users in MockMvc
Test classes annotated with `@SpringBootTest` and `@AutoConfigureMockMvc` can be used to test REST controllers. Theoretically it should be possible configure a fake principal to each test request using the following notation:
```java
mockMvc.perform(post("/endpoint").principal(myFakeUser));
```
However it seems that the method is ignored if the principal is set using a custom servlet filter, like in our case (see `TokenFilter` registration defined in `VospaceApplication` class).
To bypass the problem a fake `TokenFilter` has been defined in `TokenFilterConfig` test class. This filter returns some fake users based on the received fake token. If you need additional test users just add them in the `getFakeUser()` method.
To use the fake filter add the following annotations to the test class:
```java
@ContextConfiguration(classes = {TokenFilterConfig.class})
@TestPropertySource(properties = "spring.main.allow-bean-definition-overriding=true")
```
Then add the fake token to the test request:
```java
mockMvc.perform(post("/endpoint").header("Authorization", "Bearer user1_token"));
```
......@@ -76,6 +76,8 @@ public class TransferController {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
jobService.setJobPhase(job, phase);
return getJobRedirect(job.getJobId());
}).orElse(ResponseEntity.notFound().build());
......
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.persistence.NodeDAO;
import java.util.ArrayList;
import java.util.List;
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.Node;
import net.ivoa.xml.vospace.v2.Property;
import net.ivoa.xml.vospace.v2.Protocol;
import net.ivoa.xml.vospace.v2.Transfer;
import org.springframework.beans.factory.annotation.Autowired;
......@@ -24,6 +29,12 @@ public class UriService {
@Autowired
private NodeDAO nodeDao;
@Autowired
private HttpServletRequest servletRequest;
@Autowired
private ServletRapClient rapClient;
public void setTransferJobResult(JobSummary job) {
List<ResultReference> results = new ArrayList<>();
......@@ -39,11 +50,12 @@ public class UriService {
Transfer transfer = getTransfer(job);
Protocol protocol = new Protocol();
protocol.setUri("ivo://ivoa.net/vospace/core#httpget");
protocol.setEndpoint(getEndpoint(job));
Protocol protocol = transfer.getProtocols().get(0);
transfer.getProtocols().add(protocol);
if (!"ivo://ivoa.net/vospace/core#httpget".equals(protocol.getUri())) {
throw new IllegalStateException("Unsupported protocol " + protocol.getUri());
}
protocol.setEndpoint(getEndpoint(job));
}
private String getEndpoint(JobSummary job) {
......@@ -58,7 +70,40 @@ public class UriService {
// TODO build the path according to node type
//
// TODO add token for authenticated access
return fileServiceUrl + relativePath + "?jobId=" + job.getJobId();
String endpoint = fileServiceUrl + relativePath + "?jobId=" + job.getJobId();
if (!"true".equals(getProperty(node, "publicread"))) {
endpoint += "&token=" + getEndpointToken(fileServiceUrl + relativePath);
}
return endpoint;
}
private String getEndpointToken(String endpoint) {
String token = ((User) servletRequest.getUserPrincipal()).getAccessToken();
if (token == null) {
// TODO: use PermissionDenied VoSpaceException
throw new IllegalStateException("Token is null");
}
TokenExchangeRequest exchangeRequest = new TokenExchangeRequest()
.setSubjectToken(token)
.setResource(endpoint);
// TODO: add audience and scope
return rapClient.exchangeToken(exchangeRequest, servletRequest);
}
private String getProperty(Node node, String propertyName) {
for (Property property : node.getProperties()) {
if (property.getUri().equals("ivo://ivoa.net/vospace/core#".concat(propertyName))) {
return property.getValue();
}
}
return null;
}
private Transfer getTransfer(JobSummary job) {
......
package it.inaf.oats.vospace;
import it.inaf.ia2.aa.ServiceLocator;
import it.inaf.ia2.aa.ServletRapClient;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
......@@ -20,4 +22,9 @@ public class VospaceApplication {
registration.addUrlPatterns("/*");
return registration;
}
@Bean
public ServletRapClient servletRapClient() {
return (ServletRapClient) ServiceLocator.getInstance().getRapClient();
}
}
package it.inaf.oats.vospace;
import it.inaf.ia2.aa.TokenFilter;
import it.inaf.ia2.aa.data.User;
import java.io.IOException;
import java.security.Principal;
import java.util.ArrayList;
import java.util.Arrays;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
@TestConfiguration
public class TokenFilterConfig {
@Bean
@Primary
public FilterRegistrationBean tokenFilterRegistration() {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(new FakeTokenFilter());
registration.addUrlPatterns("/*");
return registration;
}
private static class FakeTokenFilter extends TokenFilter {
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
String authHeader = request.getHeader("Authorization");
if (authHeader != null) {
if (authHeader.startsWith("Bearer ")) {
String token = authHeader.substring("Bearer ".length());
HttpServletRequestWrapper requestWithPrincipal = new RequestWithPrincipal(request, getFakeUser(token));
chain.doFilter(requestWithPrincipal, response);
return;
}
}
chain.doFilter(getAnonymousServletRequest(request), response);
}
private User getFakeUser(String token) {
User user = new User();
switch (token) {
case "user1_token":
user.setUserId("user1").setUserLabel("User1");
break;
case "user2_token":
user.setUserId("user2").setUserLabel("User2").setGroups(Arrays.asList("group1", "group2"));
break;
default:
throw new IllegalArgumentException("Fake user not configured for token " + token);
}
user.setAccessToken(token);
return user;
}
private static HttpServletRequestWrapper getAnonymousServletRequest(HttpServletRequest request) {
User anonymousUser = new User()
.setUserId("anonymous")
.setUserLabel("Anonymous")
.setGroups(new ArrayList<>());
return new RequestWithPrincipal(request, anonymousUser);
}
private static class RequestWithPrincipal extends HttpServletRequestWrapper {
private final User user;
public RequestWithPrincipal(HttpServletRequest request, User user) {
super(request);
this.user = user;
}
@Override
public Principal getUserPrincipal() {
return user;
}
}
}
}
package it.inaf.oats.vospace;
import it.inaf.ia2.aa.data.User;
import static it.inaf.oats.vospace.VOSpaceXmlTestUtil.loadDocument;
import it.inaf.oats.vospace.persistence.JobDAO;
import it.inaf.oats.vospace.persistence.NodeDAO;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.Optional;
import net.ivoa.xml.uws.v1.ExecutionPhase;
import net.ivoa.xml.uws.v1.JobSummary;
import net.ivoa.xml.vospace.v2.DataNode;
import net.ivoa.xml.vospace.v2.Node;
import net.ivoa.xml.vospace.v2.Property;
import net.ivoa.xml.vospace.v2.Protocol;
import net.ivoa.xml.vospace.v2.Transfer;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.matchesPattern;
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.times;
import static org.mockito.Mockito.verify;
......@@ -15,28 +28,157 @@ import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMock
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.w3c.dom.Document;
@SpringBootTest
@AutoConfigureMockMvc
@ContextConfiguration(classes = {TokenFilterConfig.class})
@TestPropertySource(properties = "spring.main.allow-bean-definition-overriding=true")
public class TransferControllerTest {
@MockBean
private JobDAO dao;
private JobDAO jobDao;
@MockBean
private NodeDAO nodeDao;
@MockBean
private TapeService tapeService;
@Autowired
private MockMvc mockMvc;
@Test
public void testPullFromVoSpaceAsync() throws Exception {
Node node = mockPublicDataNode();
when(nodeDao.listNode(eq("/mynode"))).thenReturn(Optional.of(node));
String requestBody = getResourceFileContent("pullFromVoSpace.xml");
String redirect = mockMvc.perform(post("/transfers?PHASE=RUN")
.content(requestBody)
.contentType(MediaType.APPLICATION_XML)
.accept(MediaType.APPLICATION_XML))
.andDo(print())
.andExpect(status().is3xxRedirection())
.andReturn().getResponse().getHeader("Location");
assertThat(redirect, matchesPattern("^/transfers/.*"));
}
@Test
public void testPullFromVoSpaceSync() throws Exception {
Node node = mockPublicDataNode();
when(nodeDao.listNode(eq("/mynode"))).thenReturn(Optional.of(node));
String requestBody = getResourceFileContent("pullFromVoSpace.xml");
String redirect = mockMvc.perform(post("/synctrans")
.content(requestBody)
.contentType(MediaType.APPLICATION_XML)
.accept(MediaType.APPLICATION_XML))
.andDo(print())
.andExpect(status().is3xxRedirection())
.andReturn().getResponse().getHeader("Location");
assertThat(redirect, matchesPattern("^/transfers/.*/results/transferDetails"));
}
@Test
public void testPullToVoSpace() throws Exception {
String requestBody = getResourceFileContent("pullToVoSpace.xml");
String redirect = mockMvc.perform(post("/transfers?PHASE=RUN")
.content(requestBody)
.contentType(MediaType.APPLICATION_XML)
.accept(MediaType.APPLICATION_XML))
.andDo(print())
.andExpect(status().is3xxRedirection())
.andReturn().getResponse().getHeader("Location");
assertThat(redirect, matchesPattern("^/transfers/.*"));
verify(tapeService, times(1)).startJob(any());
}
@Test
public void testSetJobPhase() throws Exception {
Node node = mockPublicDataNode();
when(nodeDao.listNode(eq("/mynode"))).thenReturn(Optional.of(node));
JobSummary job = getFakePendingJob();
when(jobDao.getJob(eq("123"))).thenReturn(Optional.of(job));
User user = new User();
user.setUserId("ownerId");
String redirect = mockMvc.perform(post("/transfers/123/phase")
.header("Authorization", "Bearer user1_token")
.param("PHASE", "RUN")
.accept(MediaType.APPLICATION_XML))
.andDo(print())
.andExpect(status().is3xxRedirection())
.andReturn().getResponse().getHeader("Location");
verify(jobDao, times(1)).updateJob(any());
assertThat(redirect, matchesPattern("^/transfers/.*"));
}
@Test
public void testGetTransferDetails() throws Exception {
JobSummary job = getFakePendingJob();
when(jobDao.getJob(eq("123"))).thenReturn(Optional.of(job));
mockMvc.perform(get("/transfers/123/results/transferDetails")
.header("Authorization", "Bearer user1_token")
.accept(MediaType.APPLICATION_XML))
.andDo(print())
.andExpect(status().isOk());
}
@Test
public void testGetJobPhase() throws Exception {
JobSummary job = getFakePendingJob();
when(jobDao.getJob(eq("123"))).thenReturn(Optional.of(job));
String phase = mockMvc.perform(get("/transfers/123/phase")
.header("Authorization", "Bearer user1_token"))
.andDo(print())
.andExpect(status().isOk())
.andReturn().getResponse().getContentAsString();
assertEquals("PENDING", phase);
}
private Node mockPublicDataNode() {
Node node = new DataNode();
Property property = new Property();
property.setUri("ivo://ivoa.net/vospace/core#publicread");
property.setValue("true");
node.getProperties().add(property);
return node;
}
@Test
public void testGetJob() throws Exception {
JobSummary job = new JobSummary();
when(dao.getJob(eq("123"))).thenReturn(Optional.of(job));
when(jobDao.getJob(eq("123"))).thenReturn(Optional.of(job));
String xml = mockMvc.perform(get("/transfers/123")
.accept(MediaType.APPLICATION_XML))
......@@ -47,6 +189,31 @@ public class TransferControllerTest {
Document doc = loadDocument(xml);
assertEquals("uws:job", doc.getDocumentElement().getNodeName());
verify(dao, times(1)).getJob(eq("123"));
verify(jobDao, times(1)).getJob(eq("123"));
}
private JobSummary getFakePendingJob() {
JobSummary job = new JobSummary();
job.setPhase(ExecutionPhase.PENDING);
job.setOwnerId("user1");
Transfer transfer = new Transfer();
transfer.setDirection("pullFromVoSpace");
transfer.setTarget("vos://example.com!vospace/mynode");
Protocol protocol = new Protocol();
protocol.setUri("ivo://ivoa.net/vospace/core#httpget");
transfer.getProtocols().add(protocol);
JobSummary.JobInfo jobInfo = new JobSummary.JobInfo();
jobInfo.getAny().add(transfer);
job.setJobInfo(jobInfo);
return job;
}
protected static String getResourceFileContent(String fileName) throws Exception {
try ( InputStream in = TransferControllerTest.class.getClassLoader().getResourceAsStream(fileName)) {
return new String(in.readAllBytes(), StandardCharsets.UTF_8);
}
}
}
package it.inaf.oats.vospace;
import it.inaf.ia2.aa.ServletRapClient;
import it.inaf.ia2.aa.data.User;
import it.inaf.oats.vospace.persistence.NodeDAO;
import java.util.Optional;
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.Node;
import net.ivoa.xml.vospace.v2.Property;
import net.ivoa.xml.vospace.v2.Transfer;
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.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import org.springframework.test.context.TestPropertySource;
@SpringBootTest
......@@ -24,16 +34,71 @@ public class UriServiceTest {
@MockBean
private NodeDAO dao;
@MockBean
private ServletRapClient rapClient;
@Autowired
private HttpServletRequest servletRequest;
@Autowired
private UriService transferService;
private UriService uriService;
@TestConfiguration
public static class TestConfig {
/**
* Necessary because MockBean doesn't work with HttpServletRequest.
*/
@Bean
@Primary
public HttpServletRequest servletRequest() {
return mock(HttpServletRequest.class);
}
}
@Test
public void testPublicUrl() {
Node node = new DataNode();
Property property = new Property();
property.setUri("ivo://ivoa.net/vospace/core#publicread");
property.setValue("true");
node.getProperties().add(property);
when(dao.listNode(eq("/mydata1"))).thenReturn(Optional.of(node));
JobSummary job = getJob();
uriService.setTransferJobResult(job);
assertEquals("http://file-service/mydata1?jobId=job-id", job.getResults().get(0).getHref());
}
@Test
public void testSimpleUrl() {
public void testPrivateUrl() {
Node node = new DataNode();
when(dao.listNode(eq("/mydata1"))).thenReturn(Optional.of(node));
User user = mock(User.class);
when(user.getAccessToken()).thenReturn("<token>");
when(servletRequest.getUserPrincipal()).thenReturn(user);
when(rapClient.exchangeToken(argThat(req -> {
assertEquals("<token>", req.getSubjectToken());
assertEquals("http://file-service/mydata1", req.getResource());
return true;
}), any())).thenReturn("<new-token>");
JobSummary job = getJob();
uriService.setTransferJobResult(job);
assertEquals("http://file-service/mydata1?jobId=job-id&token=<new-token>", job.getResults().get(0).getHref());
}
private JobSummary getJob() {
Transfer transfer = new Transfer();
transfer.setTarget("vos://example.com!vospace/mydata1");
......@@ -45,8 +110,6 @@ public class UriServiceTest {
job.setJobInfo(jobInfo);
transferService.setTransferJobResult(job);
assertEquals("http://file-service/mydata1?jobId=job-id", job.getResults().get(0).getHref());
return job;
}
}
<vos:transfer xmlns:vos="http://www.ivoa.net/xml/VOSpace/v2.0" version="2.1">
<vos:target>vos://example.com!vospace/mynode</vos:target>
<vos:direction>pullFromVoSpace</vos:direction>
<vos:protocol uri="ivo://ivoa.net/vospace/core#httpget" />
</vos:transfer>
\ No newline at end of file
<vos:transfer xmlns:vos="http://www.ivoa.net/xml/VOSpace/v2.0" version="2.1">
<vos:target>vos://example.com!vospace/mynode</vos:target>