Skip to content
Snippets Groups Projects
Commit 802fda65 authored by Sonia Zorba's avatar Sonia Zorba
Browse files

Inital commit

parents
No related branches found
No related tags found
No related merge requests found
Showing
with 719 additions and 0 deletions
HELP.md
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/
nbactions.xml
### VS Code ###
.vscode/
# File service for VOSpace
This service queries the same database used by VOSpace (`file_catalog`).
It provides functionalities for downloading and uploading files.
pom.xml 0 → 100644
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.6.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>it.inaf.ia2</groupId>
<artifactId>vospace-file-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>vospace-file-service</name>
<description>VOSpace File service</description>
<properties>
<java.version>14</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>rap-client</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
package it.inaf.ia2.transfer;
import it.inaf.ia2.aa.jwt.JwksClient;
import it.inaf.ia2.aa.jwt.TokenParser;
import it.inaf.ia2.transfer.auth.TokenFilter;
import java.net.URI;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
@SpringBootApplication
public class FileServiceApplication {
@Value("${jwks_uri}")
private String jwksUri;
public static void main(String[] args) {
SpringApplication.run(FileServiceApplication.class, args);
}
@Bean
public TokenParser tokenParser() {
JwksClient jwksClient = new JwksClient(URI.create(jwksUri));
return new TokenParser(jwksClient);
}
@Bean
public FilterRegistrationBean tokenFilterRegistration(TokenParser tokenParser) {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(new TokenFilter(tokenParser));
registration.addUrlPatterns("/*");
return registration;
}
}
package it.inaf.ia2.transfer.auth;
import java.util.Collections;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
@Component
public class GmsClient {
@Value("${gms_base_url}")
private String gmsBaseUrl;
private final RestTemplate restTemplate;
@Autowired
public GmsClient() {
restTemplate = new RestTemplate();
}
@Cacheable("gms_cache")
public boolean isMemberOf(String token, String group) {
String url = gmsBaseUrl + "/vo/search/" + group;
String gmsResponse = restTemplate.exchange(url, HttpMethod.GET, getEntity(token), String.class).getBody();
if (gmsResponse == null) {
return false;
}
return group.equals(gmsResponse.replace("\n", ""));
}
private <T> HttpEntity<T> getEntity(String token) {
HttpHeaders headers = new HttpHeaders();
headers.setAccept(Collections.singletonList(MediaType.TEXT_PLAIN));
headers.add("Authorization", "Bearer " + token);
return new HttpEntity<>(null, headers);
}
}
package it.inaf.ia2.transfer.auth;
import it.inaf.ia2.aa.jwt.InvalidTokenException;
import it.inaf.ia2.aa.jwt.TokenParser;
import java.io.IOException;
import java.security.Principal;
import java.util.Map;
import javax.servlet.Filter;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class TokenFilter implements Filter {
private static final Logger LOG = LoggerFactory.getLogger(TokenFilter.class);
private final TokenParser tokenParser;
public TokenFilter(TokenParser tokenParser) {
this.tokenParser = tokenParser;
}
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
String token = getToken(request);
TokenPrincipal tokenPrincipal;
try {
tokenPrincipal = getTokenPrincipal(token);
} catch (InvalidTokenException ex) {
response.sendError(401, "Unauthorized: " + ex.getMessage());
return;
}
RequestWithPrincipal requestWrapper = new RequestWithPrincipal(request, tokenPrincipal);
chain.doFilter(requestWrapper, response);
}
private String getToken(HttpServletRequest request) {
String token = getTokenFromHeader(request);
if (token == null) {
// get token from query string
token = request.getParameter("token");
}
return token;
}
private String getTokenFromHeader(HttpServletRequest request) {
LOG.trace("Loading token from header");
String authHeader = request.getHeader("Authorization");
if (authHeader != null) {
if (authHeader.startsWith("Bearer")) {
return authHeader.substring("Bearer".length() + 1).trim();
} else {
LOG.warn("Detected non-Bearer Authorization header");
}
}
return null;
}
private TokenPrincipal getTokenPrincipal(String token) {
if (token == null) {
// anonymous
return new TokenPrincipal();
}
Map<String, Object> claims = tokenParser.getClaims(token);
String userId = (String) claims.get("sub");
if (userId == null) {
throw new InvalidTokenException("Missing sub claim");
}
return new TokenPrincipal(userId, token);
}
private static class RequestWithPrincipal extends HttpServletRequestWrapper {
private final TokenPrincipal tokenPrincipal;
public RequestWithPrincipal(HttpServletRequest request, TokenPrincipal tokenPrincipal) {
super(request);
this.tokenPrincipal = tokenPrincipal;
}
@Override
public Principal getUserPrincipal() {
return tokenPrincipal;
}
}
}
package it.inaf.ia2.transfer.auth;
import java.security.Principal;
public class TokenPrincipal implements Principal {
private final String name;
private final String token;
public TokenPrincipal() {
this("anonymous", null);
}
public TokenPrincipal(String userId, String token) {
this.name = userId;
this.token = token;
}
@Override
public String getName() {
return name;
}
public String getToken() {
return token;
}
}
package it.inaf.ia2.transfer.controller;
import java.util.List;
public class FileInfo {
private String osRelPath;
private boolean isPublic;
private List<String> groupRead;
private List<String> groupWrite;
private boolean asyncTrans;
public String getOsRelPath() {
return osRelPath;
}
public void setOsRelPath(String osRelPath) {
this.osRelPath = osRelPath;
}
public boolean isIsPublic() {
return isPublic;
}
public void setIsPublic(boolean isPublic) {
this.isPublic = isPublic;
}
public List<String> getGroupRead() {
return groupRead;
}
public void setGroupRead(List<String> groupRead) {
this.groupRead = groupRead;
}
public List<String> getGroupWrite() {
return groupWrite;
}
public void setGroupWrite(List<String> groupWrite) {
this.groupWrite = groupWrite;
}
public boolean isAsyncTrans() {
return asyncTrans;
}
public void setAsyncTrans(boolean asyncTrans) {
this.asyncTrans = asyncTrans;
}
}
package it.inaf.ia2.transfer.controller;
import it.inaf.ia2.transfer.auth.GmsClient;
import it.inaf.ia2.transfer.auth.TokenPrincipal;
import it.inaf.ia2.transfer.persistence.FileDAO;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UncheckedIOException;
import java.util.Optional;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR;
import static org.springframework.http.HttpStatus.NOT_FOUND;
import static org.springframework.http.HttpStatus.UNAUTHORIZED;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class GetFileController {
@Value("${path_prefix}")
String pathPrefix;
@Autowired
private FileDAO fileDAO;
@Autowired
private GmsClient gmsClient;
@Autowired
private HttpServletRequest request;
@Autowired
private HttpServletResponse response;
@GetMapping("/**")
public ResponseEntity<?> getFile() {
String path = request.getServletPath();
Optional<FileInfo> optFileInfo = fileDAO.getFileInfo(path);
if (optFileInfo.isPresent()) {
FileInfo fileInfo = optFileInfo.get();
if (!fileInfo.isIsPublic() && !privateButDownloadable(fileInfo)) {
return new ResponseEntity<>("Unauthorized", UNAUTHORIZED);
}
return getFileResponse(fileInfo);
} else {
return new ResponseEntity<>("File " + path + " not found", NOT_FOUND);
}
}
private boolean privateButDownloadable(FileInfo fileInfo) {
String token = ((TokenPrincipal) request.getUserPrincipal()).getToken();
if (token == null) {
return false;
}
// TODO: configure cache
if (fileInfo.getGroupRead() == null) {
return false;
}
for (String group : fileInfo.getGroupRead()) {
if (gmsClient.isMemberOf(token, group)) {
return true;
}
}
return false;
}
private ResponseEntity<?> getFileResponse(FileInfo fileInfo) {
String path = pathPrefix + fileInfo.getOsRelPath();
if (fileInfo.isAsyncTrans()) {
// TODO: add /retrieve part
}
File file = new File(path);
if (!file.exists()) {
return new ResponseEntity<>("File " + file.getName() + " not found", NOT_FOUND);
}
if (!file.canRead()) {
return new ResponseEntity<>("File " + file.getName() + " is not readable", INTERNAL_SERVER_ERROR);
}
response.setHeader("Content-Disposition", "attachment; filename=" + file.getName());
response.setHeader("Content-Length", String.valueOf(file.length()));
byte[] bytes = new byte[1024];
try ( OutputStream out = response.getOutputStream(); InputStream is = new FileInputStream(file)) {
int read;
while ((read = is.read(bytes)) != -1) {
out.write(bytes, 0, read);
}
} catch (IOException ex) {
throw new UncheckedIOException(ex);
}
return null;
}
}
package it.inaf.ia2.transfer.persistence;
import javax.sql.DataSource;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class DataSourcesConfig {
@Bean
@ConfigurationProperties(prefix = "file-catalog.datasource")
public DataSource fileCatalogDataSource() {
return DataSourceBuilder.create().build();
}
}
package it.inaf.ia2.transfer.persistence;
import it.inaf.ia2.transfer.controller.FileInfo;
import java.sql.Array;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
@Repository
public class FileDAO {
private final JdbcTemplate jdbcTemplate;
@Autowired
public FileDAO(DataSource fileCatalogDatasource) {
this.jdbcTemplate = new JdbcTemplate(fileCatalogDatasource);
}
public Optional<FileInfo> getFileInfo(String virtualPath) {
String sql = "select os_path, is_public, group_read, group_write, async_trans from\n"
+ "node n join node_path p on n.node_id = p.node_id\n"
+ "and vos_path = ?";
FileInfo fileInfo = jdbcTemplate.query(conn -> {
PreparedStatement ps = conn.prepareStatement(sql);
ps.setString(1, virtualPath);
return ps;
}, rs -> {
if (rs.next()) {
FileInfo fi = new FileInfo();
fi.setOsRelPath(rs.getString("os_path"));
fi.setIsPublic(rs.getBoolean("is_public"));
fi.setGroupRead(toList(rs.getArray("group_read")));
fi.setGroupWrite(toList(rs.getArray("group_write")));
fi.setAsyncTrans(rs.getBoolean("async_trans"));
return fi;
}
return null;
});
return Optional.ofNullable(fileInfo);
}
private List<String> toList(Array array) throws SQLException {
if (array == null) {
return new ArrayList<>();
}
return Arrays.asList((String[]) array.getArray());
}
}
server.port=8080
server.servlet.context-path=/
file-catalog.datasource.jdbc-url=jdbc:postgresql://127.0.0.1:5432/vospace_testdb
file-catalog.datasource.username=postgres
file-catalog.datasource.password=
gms_base_url=http://localhost:8082/gms
jwks_uri=http://localhost/rap-ia2/auth/oidc/jwks
path_prefix=/tmp
package it.inaf.ia2.transfer;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class FileServiceApplicationTests {
@Test
void contextLoads() {
}
}
package it.inaf.ia2.transfer.controller;
import it.inaf.ia2.aa.jwt.TokenParser;
import it.inaf.ia2.transfer.auth.GmsClient;
import it.inaf.ia2.transfer.persistence.FileDAO;
import java.io.File;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import org.assertj.core.util.Files;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.verify;
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.mock.mockito.MockBean;
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.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@SpringBootTest
@AutoConfigureMockMvc
@TestPropertySource(properties = {"path_prefix=/"})
public class GetFileControllerTest {
@MockBean
private TokenParser tokenParser;
@MockBean
private GmsClient gmsClient;
@MockBean
private FileDAO fileDao;
@Autowired
private MockMvc mockMvc;
private File tempFile;
@BeforeEach
public void init() {
tempFile = Files.newTemporaryFile();
}
@AfterEach
public void cleanup() {
tempFile.delete();
}
@Test
public void getPublicFile() throws Exception {
FileInfo fileInfo = new FileInfo();
fileInfo.setOsRelPath(tempFile.getAbsolutePath());
fileInfo.setIsPublic(true);
when(fileDao.getFileInfo(any())).thenReturn(Optional.of(fileInfo));
mockMvc.perform(get("/myfile"))
.andDo(print())
.andExpect(status().isOk());
}
@Test
public void testFileNotFoundInDb() throws Exception {
mockMvc.perform(get("/myfile"))
.andDo(print())
.andExpect(status().isNotFound());
}
@Test
public void testFileNotFoundOnDisk() throws Exception {
FileInfo fileInfo = new FileInfo();
fileInfo.setOsRelPath("/this/doesnt/exists");
fileInfo.setIsPublic(true);
when(fileDao.getFileInfo(any())).thenReturn(Optional.of(fileInfo));
mockMvc.perform(get("/myfile"))
.andDo(print())
.andExpect(status().isNotFound());
}
@Test
public void getPrivateFileTokenInHeader() throws Exception {
prepareMocksForPrivateFile();
mockMvc.perform(get("/myfile")
.header("Authorization", "Bearer: <token>"))
.andDo(print())
.andExpect(status().isOk());
verify(gmsClient).isMemberOf(eq("<token>"), eq("group1"));
}
@Test
public void getPrivateFileTokenInQueryString() throws Exception {
prepareMocksForPrivateFile();
mockMvc.perform(get("/myfile?token=<token>"))
.andDo(print())
.andExpect(status().isOk());
verify(gmsClient).isMemberOf(eq("<token>"), eq("group1"));
}
private void prepareMocksForPrivateFile() {
Map<String, Object> claims = new HashMap<>();
claims.put("sub", "123");
when(tokenParser.getClaims(any())).thenReturn(claims);
when(gmsClient.isMemberOf(any(), any())).thenReturn(true);
FileInfo fileInfo = new FileInfo();
fileInfo.setOsRelPath(tempFile.getAbsolutePath());
fileInfo.setGroupRead(Collections.singletonList("group1"));
when(fileDao.getFileInfo(any())).thenReturn(Optional.of(fileInfo));
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment