Skip to content

Commit

Permalink
Merge pull request #4 from qbicsoftware/feature/dm-1122-enable-pat-to…
Browse files Browse the repository at this point in the history
…ken-auth

Authenticate using personal access tokens
  • Loading branch information
KochTobi authored Apr 3, 2024
2 parents cdf0151 + 3aa125a commit 403d859
Show file tree
Hide file tree
Showing 24 changed files with 888 additions and 30 deletions.
2 changes: 1 addition & 1 deletion openbis-connector/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>3.2.2</version>
<version>3.2.4</version>
<type>pom</type>
<scope>import</scope>
</dependency>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,25 @@
import ch.ethz.sis.openbis.generic.asapi.v3.IApplicationServerApi;
import ch.ethz.sis.openbis.generic.dssapi.v3.IDataStoreServerApi;
import ch.systemsx.cisd.common.spring.HttpInvokerUtils;
import java.time.Duration;
import java.time.temporal.ChronoUnit;

/**
* A provider for openbis apis v3
*/
public class ApiV3 {

private static final Duration TIMEOUT = Duration.of(5, ChronoUnit.DAYS);

private ApiV3() {
//hide the implicit constructor
}

public static IApplicationServerApi applicationServer(String url) {
return HttpInvokerUtils.createServiceStub(IApplicationServerApi.class, url + IApplicationServerApi.SERVICE_URL, 100_000L);
return HttpInvokerUtils.createServiceStub(IApplicationServerApi.class, url + IApplicationServerApi.SERVICE_URL, TIMEOUT.toMillis());
}
public static IDataStoreServerApi dataStoreServer(String url) {
return HttpInvokerUtils.createStreamSupportingServiceStub(IDataStoreServerApi.class, url + IDataStoreServerApi.SERVICE_URL, 100_000L);
return HttpInvokerUtils.createStreamSupportingServiceStub(IDataStoreServerApi.class, url + IDataStoreServerApi.SERVICE_URL, TIMEOUT.toMillis());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import ch.ethz.sis.openbis.generic.dssapi.v3.dto.datasetfile.download.DataSetFileDownload;
import ch.ethz.sis.openbis.generic.dssapi.v3.dto.datasetfile.download.DataSetFileDownloadReader;
import java.io.InputStream;
import java.util.Optional;
import life.qbic.data_download.measurements.api.DataFile;
import life.qbic.data_download.measurements.api.FileInfo;
import life.qbic.data_download.measurements.api.MeasurementDataReader;
Expand All @@ -16,8 +17,11 @@
public class DatasetFileStreamReaderImpl implements MeasurementDataReader {

private DataSetFileDownloadReader dataSetFileDownloadReader;
public DatasetFileStreamReaderImpl() {
private final String ignoredPrefix;

public DatasetFileStreamReaderImpl(String ignoredPrefix) {
dataSetFileDownloadReader = null;
this.ignoredPrefix = Optional.ofNullable(ignoredPrefix).orElse("");
}


Expand Down Expand Up @@ -51,7 +55,7 @@ public DataFile nextDataFile() {
.toEpochMilli() : -1;
long lastModifiedMillis = nonNull(dataStore) ? dataStore.getModificationDate().toInstant()
.toEpochMilli() : -1;
FileInfo fileInfo = new FileInfo(fileDownload.getDataSetFile().getPath(),
FileInfo fileInfo = new FileInfo(fileDownload.getDataSetFile().getPath().replaceFirst(ignoredPrefix, ""),
fileDownload.getDataSetFile().getFileLength(),
Integer.toUnsignedLong(fileDownload.getDataSetFile().getChecksumCRC32()),
creationMillis,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package life.qbic.data_download.openbis;

import static org.junit.jupiter.api.Assertions.*;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

Expand Down
21 changes: 19 additions & 2 deletions rest-api/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.2</version>
<version>3.2.4</version>
<relativePath/>
</parent>

<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<spring.boot.version>3.2.2</spring.boot.version>
<spring.boot.version>3.2.4</spring.boot.version>
</properties>

<dependencies>
Expand All @@ -32,6 +32,23 @@
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,9 @@ public SessionFactory sessionFactory(
}

@Bean("measurementDataReader")
public MeasurementDataReader measurementDataReader() {
return new DatasetFileStreamReaderImpl();
public MeasurementDataReader measurementDataReader(
@Value("${openbis.filename.ignored-prefix}") String ignoredPathPrefix) {
return new DatasetFileStreamReaderImpl(ignoredPathPrefix);
}

@Bean("errorMessageSource")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package life.qbic.data_download.rest.config;

import java.time.Duration;
import java.util.concurrent.Executor;
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
import org.springframework.aop.interceptor.SimpleAsyncUncaughtExceptionHandler;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.web.servlet.config.annotation.AsyncSupportConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;


@Configuration
@EnableAsync
public class AsyncDownloadConfig implements AsyncConfigurer {

@Override
@Bean("taskExecutor")
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
threadPoolTaskExecutor.setCorePoolSize(2);
threadPoolTaskExecutor.setMaxPoolSize(2);
threadPoolTaskExecutor.setQueueCapacity(200);
threadPoolTaskExecutor.setThreadNamePrefix("download - ");
threadPoolTaskExecutor.initialize();
return threadPoolTaskExecutor;
}

@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return new SimpleAsyncUncaughtExceptionHandler();
}

@Bean
public WebMvcConfigurer webMvcConfigurer(
@Qualifier("taskExecutor") final AsyncTaskExecutor taskExecutor,
@Value("${spring.mvc.async.request-timeout}") Duration timeout) {
return new WebMvcConfigurer() {
@Override
public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
configurer.setDefaultTimeout(timeout.toMillis()).setTaskExecutor(taskExecutor);

}
};
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package life.qbic.data_download.rest.config;

import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.enums.SecuritySchemeIn;
import io.swagger.v3.oas.annotations.enums.SecuritySchemeType;
import io.swagger.v3.oas.annotations.info.Info;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.security.SecurityScheme;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.security.SecurityScheme.In;
import io.swagger.v3.oas.models.security.SecurityScheme.Type;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@OpenAPIDefinition(info = @Info(title = "My API", version = "v1"),
security = @SecurityRequirement(name = "personal_access_token")) //globally set this
public class OpenApiConfig {

@Bean
public OpenAPI customOpenAPI(
@Value("${server.download.token-name}") String tokenName
) {
io.swagger.v3.oas.models.security.SecurityScheme securityScheme = new io.swagger.v3.oas.models.security.SecurityScheme()
.type(Type.APIKEY)
.in(In.HEADER)
.description(
"A personal access token (PAT) obtained through by the data manager. Please prefix your token with '"
+ tokenName + " " + "' e.g. '" + tokenName + " abcdefg1234'.")
.name("Authorization");
var securityComponent = new Components()
.addSecuritySchemes("personal_access_token", securityScheme);

return new OpenAPI()
.components(securityComponent);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package life.qbic.data_download.rest.config;

import life.qbic.data_download.rest.security.QBiCTokenAuthenticationFilter;
import life.qbic.data_download.rest.security.QBiCTokenAuthenticationProvider;
import life.qbic.data_download.rest.security.QBiCTokenMatcher;
import life.qbic.data_download.rest.security.TokenMatcher;
import life.qbic.data_download.rest.security.jpa.token.EncodedAccessTokenRepository;
import life.qbic.data_download.rest.security.jpa.user.UserDetailsRepository;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;

/**
* The security config setting up endpoint security for the controllers.
*/
@Configuration
@EnableWebSecurity
public class SecurityConfig {

private final EncodedAccessTokenRepository encodedAccessTokenRepository;
private final UserDetailsRepository userDetailsRepository;
private final String[] ignoredEndpoints = {
"/v3/api-docs/**",
"/swagger-ui/**",
"/swagger-ui.html"
};

public SecurityConfig(EncodedAccessTokenRepository encodedAccessTokenRepository,
UserDetailsRepository userDetailsRepository) {
this.encodedAccessTokenRepository = encodedAccessTokenRepository;
this.userDetailsRepository = userDetailsRepository;
}


@Bean("accessTokenEncoder")
public TokenMatcher tokenEncoder() {
return new QBiCTokenMatcher();
}

@Bean("tokenAuthenticationProvider")
public QBiCTokenAuthenticationProvider authenticationProvider(
@Qualifier("accessTokenEncoder") TokenMatcher tokenMatcher) {
return new QBiCTokenAuthenticationProvider(tokenMatcher, encodedAccessTokenRepository, userDetailsRepository);
}

@Bean("tokenAuthenticationManager")
public AuthenticationManager authenticationManager(
@Qualifier("tokenAuthenticationProvider") QBiCTokenAuthenticationProvider authenticationProvider) {
return new ProviderManager(authenticationProvider);
}

@Bean("tokenAuthenticationFilter")
public QBiCTokenAuthenticationFilter authenticationFilter(
@Qualifier("tokenAuthenticationManager") AuthenticationManager authenticationManager,
@Value("${server.download.token-name}") String tokenName) {
return new QBiCTokenAuthenticationFilter(authenticationManager, tokenName);
}



@Bean
public SecurityFilterChain apiFilterChain(HttpSecurity http,
@Qualifier("tokenAuthenticationProvider") QBiCTokenAuthenticationProvider authenticationProvider,
@Qualifier("tokenAuthenticationFilter") QBiCTokenAuthenticationFilter tokenAuthenticationFilter) throws Exception {
http
.authorizeHttpRequests(authorizedRequest ->
authorizedRequest
.requestMatchers(ignoredEndpoints)
.permitAll())
// //require https
// .requiresChannel(channel -> channel.anyRequest().requiresSecure())
.authenticationProvider(authenticationProvider)
.addFilterAt(tokenAuthenticationFilter, BasicAuthenticationFilter.class)
.authorizeHttpRequests(authorizedRequest ->
authorizedRequest
.requestMatchers("/download/measurements/**")
.authenticated()
);
// .access(new WebExpressionAuthorizationManager("hasPermission(//TODO)")));

return http.build();
}


@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return web -> web.ignoring().requestMatchers(ignoredEndpoints);
}
}
Loading

0 comments on commit 403d859

Please sign in to comment.