Skip to content

Commit

Permalink
Merge pull request #5 from qbicsoftware/feature/dm-1195-check-authori…
Browse files Browse the repository at this point in the history
…zation-for-measurement-access

Authorize requests based on measurement access
  • Loading branch information
KochTobi authored Apr 5, 2024
2 parents 8eeb365 + 740dbca commit 53fa873
Show file tree
Hide file tree
Showing 18 changed files with 463 additions and 62 deletions.
4 changes: 4 additions & 0 deletions rest-api/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-acl</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package life.qbic.data_download.rest;

import java.util.List;
import life.qbic.data_download.measurements.api.MeasurementDataReader;
import life.qbic.data_download.openbis.DatasetFileStreamReaderImpl;
import life.qbic.data_download.openbis.OpenBisConnector;
import life.qbic.data_download.openbis.SessionFactory;
import life.qbic.data_download.rest.download.MeasurementDataReaderFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.MessageSource;
Expand Down Expand Up @@ -34,12 +34,13 @@ public SessionFactory sessionFactory(
return new SessionFactory(applicationServerUrl, userName, password);
}

@Bean("measurementDataReader")
public MeasurementDataReader measurementDataReader(

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

}
@Bean("errorMessageSource")
public MessageSource errorMessageSource() {
ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,54 @@
package life.qbic.data_download.rest.config;

import static org.springframework.security.authorization.AuthorizationManagers.anyOf;

import javax.sql.DataSource;
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.RequestAuthorizationManagerFactory;
import life.qbic.data_download.rest.security.TokenMatcher;
import life.qbic.data_download.rest.security.acl.MeasurementMappingService;
import life.qbic.data_download.rest.security.acl.QBiCMeasurementMappingService;
import life.qbic.data_download.rest.security.acl.QbicPermissionEvaluator;
import life.qbic.data_download.rest.security.jpa.measurement.NGSMeasurementRepository;
import life.qbic.data_download.rest.security.jpa.measurement.ProteomicsMeasurementRepository;
import life.qbic.data_download.rest.security.jpa.token.EncodedAccessTokenRepository;
import life.qbic.data_download.rest.security.jpa.user.UserDetailsRepository;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.CacheManager;
import org.springframework.cache.concurrent.ConcurrentMapCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import org.springframework.security.access.PermissionEvaluator;
import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
import org.springframework.security.acls.domain.AclAuthorizationStrategy;
import org.springframework.security.acls.domain.AclAuthorizationStrategyImpl;
import org.springframework.security.acls.domain.AuditLogger;
import org.springframework.security.acls.domain.DefaultPermissionGrantingStrategy;
import org.springframework.security.acls.domain.SpringCacheBasedAclCache;
import org.springframework.security.acls.jdbc.BasicLookupStrategy;
import org.springframework.security.acls.jdbc.JdbcMutableAclService;
import org.springframework.security.acls.jdbc.LookupStrategy;
import org.springframework.security.acls.model.AclCache;
import org.springframework.security.acls.model.AclService;
import org.springframework.security.acls.model.AuditableAccessControlEntry;
import org.springframework.security.acls.model.MutableAclService;
import org.springframework.security.acls.model.PermissionGrantingStrategy;
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.core.authority.SimpleGrantedAuthority;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.access.expression.DefaultHttpSecurityExpressionHandler;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.util.Assert;

/**
* The security config setting up endpoint security for the controllers.
Expand All @@ -25,20 +57,12 @@
@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() {
Expand All @@ -47,8 +71,11 @@ public TokenMatcher tokenEncoder() {

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

@Bean("tokenAuthenticationManager")
Expand All @@ -65,11 +92,12 @@ public QBiCTokenAuthenticationFilter authenticationFilter(
}



@Bean
public SecurityFilterChain apiFilterChain(HttpSecurity http,
@Qualifier("tokenAuthenticationProvider") QBiCTokenAuthenticationProvider authenticationProvider,
@Qualifier("tokenAuthenticationFilter") QBiCTokenAuthenticationFilter tokenAuthenticationFilter) throws Exception {
@Qualifier("tokenAuthenticationFilter") QBiCTokenAuthenticationFilter tokenAuthenticationFilter,
@Qualifier("authorizationManagerFactory") RequestAuthorizationManagerFactory requestAuthorizationManagerFactory
) throws Exception {
http
.authorizeHttpRequests(authorizedRequest ->
authorizedRequest
Expand All @@ -81,17 +109,126 @@ public SecurityFilterChain apiFilterChain(HttpSecurity http,
.addFilterAt(tokenAuthenticationFilter, BasicAuthenticationFilter.class)
.authorizeHttpRequests(authorizedRequest ->
authorizedRequest
.requestMatchers("/download/measurements/**")
.authenticated()
);
// .access(new WebExpressionAuthorizationManager("hasPermission(//TODO)")));
.requestMatchers("/download/measurements/{measurementId}")
.access(anyOf(
requestAuthorizationManagerFactory.spel(
"hasPermission(#measurementId, 'qbic.measurement', 'READ')")
)));

return http.build();
}

@Bean("authorizationManagerFactory")
public RequestAuthorizationManagerFactory authorizationManagerFactory(
@Qualifier("permissionEvaluator") PermissionEvaluator permissionEvaluator) {

DefaultHttpSecurityExpressionHandler expressionHandler = new DefaultHttpSecurityExpressionHandler();
expressionHandler.setPermissionEvaluator(permissionEvaluator);
return new RequestAuthorizationManagerFactory(expressionHandler);
}

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

// ACL
@Bean("auditLogger")
public AuditLogger auditLogger() {
var logger = LoggerFactory.getLogger(SecurityConfig.class);
return (granted, ace) -> {
Assert.notNull(ace, "AccessControlEntry required");
if (ace instanceof AuditableAccessControlEntry auditableAce) {
if (!granted) {
logger.info("DENIED due to ACE: %s".formatted(ace));
} else {
auditableAce.isAuditSuccess();
}
}
};
}

@Bean("aclAuthorizationStrategy")
public AclAuthorizationStrategy aclAuthorizationStrategy() {
return new AclAuthorizationStrategyImpl(
new SimpleGrantedAuthority("acl:change-owner"), //give this to ROLE_ADMIN
new SimpleGrantedAuthority("acl:change-audit"), // give this to ROLE_ADMIN
new SimpleGrantedAuthority("acl:change-access")
//give this to ROLE_ADMIN, ROLE_PROJECT_MANAGER
);
}

@Bean("permissionGrantingStrategy")
public PermissionGrantingStrategy permissionGrantingStrategy() {
return new DefaultPermissionGrantingStrategy(auditLogger());
}

@Bean
protected AclCache aclCache() {
CacheManager cacheManager = new ConcurrentMapCacheManager();
return new SpringCacheBasedAclCache(
cacheManager.getCache("acl_cache"),
permissionGrantingStrategy(),
aclAuthorizationStrategy());
}

@Bean("securityDataSource")
public DataSource dataSource(
@Value("${qbic.access-management.datasource.url}") String url,
@Value("${qbic.access-management.datasource.username}") String name,
@Value("${qbic.access-management.datasource.password}") String password) {
var ds = new DriverManagerDataSource();
ds.setUrl(url);
ds.setUsername(name);
ds.setPassword(password);
return ds;
}

@Bean("idSupportingLookupStrategy")
public LookupStrategy lookupStrategy(
@Qualifier("securityDataSource") DataSource dataSource) {
BasicLookupStrategy basicLookupStrategy = new BasicLookupStrategy(
dataSource,
aclCache(),
aclAuthorizationStrategy(),
auditLogger()
);
basicLookupStrategy.setAclClassIdSupported(true);
return basicLookupStrategy;
}


@Bean("aclService")
public MutableAclService mutableAclService(
@Qualifier("securityDataSource") DataSource dataSource,
@Qualifier("idSupportingLookupStrategy") LookupStrategy lookupStrategy) {
JdbcMutableAclService jdbcMutableAclService = new JdbcMutableAclService(dataSource,
lookupStrategy, aclCache());
// allow for non-long type ids
jdbcMutableAclService.setAclClassIdSupported(true);

return jdbcMutableAclService;
}

@Bean("measurementMappingService")
public MeasurementMappingService measurementMappingService(
ProteomicsMeasurementRepository proteomicsMeasurementRepository,
NGSMeasurementRepository ngsMeasurementRepository) {
return new QBiCMeasurementMappingService(ngsMeasurementRepository, proteomicsMeasurementRepository);
}

@Bean("permissionEvaluator")
public PermissionEvaluator permissionEvaluator(
@Qualifier("aclService") AclService aclService,
@Qualifier("measurementMappingService") MeasurementMappingService measurementMappingService) {
return new QbicPermissionEvaluator(aclService, measurementMappingService);
}

@Bean
public MethodSecurityExpressionHandler defaultMethodSecurityExpressionHandler(
@Qualifier("permissionEvaluator") PermissionEvaluator permissionEvaluator) {
var expressionHandler = new DefaultMethodSecurityExpressionHandler();
expressionHandler.setPermissionEvaluator(permissionEvaluator);
return expressionHandler;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import java.io.OutputStream;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.UUID;
import life.qbic.data_download.measurements.api.DataFile;
import life.qbic.data_download.measurements.api.MeasurementData;
import life.qbic.data_download.measurements.api.MeasurementDataProvider;
Expand All @@ -34,29 +35,21 @@
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;

/**
* TODO!
* <b>short description</b>
*
* <p>detailed description</p>
*
* @since <version tag>
*/
@RestController
@RequestMapping(path = "/download")
public class DownloadController {

private final MeasurementDataProvider measurementDataProvider;
private final MeasurementDataReader measurementDataReader;
private final MeasurementDataReaderFactory measurementDataReaderFactory;

private static final Logger log = getLogger(DownloadController.class);

public DownloadController(
@Qualifier("measurementDataProvider") MeasurementDataProvider measurementDataProvider,
@Qualifier("measurementDataReader") MeasurementDataReader measurementDataReader
@Qualifier("measurementDataReaderFactory") MeasurementDataReaderFactory measurementDataReaderFactory
) {
this.measurementDataProvider = measurementDataProvider;
this.measurementDataReader = measurementDataReader;
this.measurementDataReaderFactory = measurementDataReaderFactory;
}


Expand All @@ -69,21 +62,29 @@ public DownloadController(
})
public ResponseEntity<StreamingResponseBody> downloadMeasurement(
@PathVariable("measurementId") String measurementId) {
String currentUser = SecurityContextHolder.getContext().getAuthentication().getName();
var requestId = "downloadMeasurement-" + UUID.randomUUID();
log.info("request %s: user %s requests measurement %s".formatted(requestId, currentUser,
measurementId));
var measurementIdentifier = new MeasurementId(measurementId);
MeasurementData measurementData = measurementDataProvider.loadData(measurementIdentifier);
if (measurementData == null) {
throw new GlobalException(ErrorCode.MEASUREMENT_NOT_FOUND, ErrorParameters.of(measurementId));
throw new GlobalException("request %s failed.".formatted(requestId),
ErrorCode.MEASUREMENT_NOT_FOUND, ErrorParameters.of(measurementId));
}

String outputFileName =
measurementId + "-"
+ LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd.hhmmss"))
+ ".zip";

StreamingResponseBody responseBody = outputStream -> writeDataToStream(measurementIdentifier,
outputStream,
measurementData,
measurementDataReader);
StreamingResponseBody responseBody = outputStream -> {
log.info("request %s: user %s started downloading measurement %s".formatted(requestId, currentUser, measurementIdentifier.id()));
writeDataToStream(measurementIdentifier,
outputStream,
measurementData,
measurementDataReaderFactory.getMeasurementDataReader());
log.info("request %s: user %s finished downloading measurement %s".formatted(requestId, currentUser, measurementIdentifier.id()));
};
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.header("Accept-Charset", "UTF-8")
Expand All @@ -94,8 +95,7 @@ public ResponseEntity<StreamingResponseBody> downloadMeasurement(

private void writeDataToStream(MeasurementId measurementId, OutputStream outputStream, MeasurementData measurementData,
MeasurementDataReader measurementDataReader) {
log.info("User %s started downloading measurement %s".formatted(
SecurityContextHolder.getContext().getAuthentication().getName(), measurementId.id()));
String currentUser = SecurityContextHolder.getContext().getAuthentication().getName();
try (final var dataStream = measurementData.stream();
final var zippedStream = BufferedZippingFunctions.zipInto(outputStream)) {
measurementDataReader.open(dataStream);
Expand All @@ -108,10 +108,12 @@ private void writeDataToStream(MeasurementId measurementId, OutputStream outputS
new FileTimes(file.fileInfo().registrationMillis(), -1,
file.fileInfo().lastModifiedMillis()));

BufferedZippingFunctions.addToZip(zippedStream, zipEntryFileInfo, file.inputStream());
BufferedZippingFunctions.addToZip(zippedStream, zipEntryFileInfo, file.inputStream(), BufferedZippingFunctions.DEFAULT_BUFFER_SIZE);
}
} catch (IOException e) {
throw new RuntimeException(e);
throw new GlobalException(
"User %s failed downloading measurement %s. %s".formatted(currentUser, measurementId.id(),
e.getMessage()), e);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package life.qbic.data_download.rest.download;

import life.qbic.data_download.measurements.api.MeasurementDataReader;

@FunctionalInterface
public interface MeasurementDataReaderFactory {

MeasurementDataReader getMeasurementDataReader();

}
Loading

0 comments on commit 53fa873

Please sign in to comment.