diff --git a/rest-api/src/main/java/life/qbic/data_download/rest/config/AsyncDownloadConfig.java b/rest-api/src/main/java/life/qbic/data_download/rest/config/AsyncDownloadConfig.java index d5c90b1..6072e2d 100644 --- a/rest-api/src/main/java/life/qbic/data_download/rest/config/AsyncDownloadConfig.java +++ b/rest-api/src/main/java/life/qbic/data_download/rest/config/AsyncDownloadConfig.java @@ -24,7 +24,7 @@ public class AsyncDownloadConfig implements AsyncConfigurer { public AsyncTaskExecutor getAsyncExecutor() { ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor(); threadPoolTaskExecutor.setCorePoolSize(2); - threadPoolTaskExecutor.setMaxPoolSize(2); + threadPoolTaskExecutor.setMaxPoolSize(5); threadPoolTaskExecutor.setQueueCapacity(200); threadPoolTaskExecutor.setThreadNamePrefix("download - "); threadPoolTaskExecutor.initialize(); diff --git a/rest-api/src/main/java/life/qbic/data_download/rest/config/SecurityConfig.java b/rest-api/src/main/java/life/qbic/data_download/rest/config/SecurityConfig.java index e380671..b576b3b 100644 --- a/rest-api/src/main/java/life/qbic/data_download/rest/config/SecurityConfig.java +++ b/rest-api/src/main/java/life/qbic/data_download/rest/config/SecurityConfig.java @@ -110,7 +110,7 @@ public SecurityFilterChain apiFilterChain(HttpSecurity http, .addFilterAt(tokenAuthenticationFilter, BasicAuthenticationFilter.class) .authorizeHttpRequests(authorizedRequest -> authorizedRequest - .requestMatchers("/measurements/{measurementId}") + .requestMatchers("measurements/{measurementId}") .access(anyOf( requestAuthorizationManagerFactory.spel( "hasPermission(#measurementId, 'qbic.measurement', 'READ')") diff --git a/rest-api/src/main/java/life/qbic/data_download/rest/download/DownloadController.java b/rest-api/src/main/java/life/qbic/data_download/rest/download/DownloadController.java index 3d8ab51..d6422e9 100644 --- a/rest-api/src/main/java/life/qbic/data_download/rest/download/DownloadController.java +++ b/rest-api/src/main/java/life/qbic/data_download/rest/download/DownloadController.java @@ -12,7 +12,9 @@ import java.io.OutputStream; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; +import java.util.Optional; import java.util.UUID; +import javax.swing.text.html.Option; import life.qbic.data_download.measurements.api.DataFile; import life.qbic.data_download.measurements.api.MeasurementData; import life.qbic.data_download.measurements.api.MeasurementDataProvider; @@ -26,6 +28,7 @@ import life.qbic.data_download.util.zip.manipulation.BufferedZippingFunctions; import org.slf4j.Logger; import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.security.core.context.SecurityContextHolder; @@ -39,15 +42,18 @@ public class DownloadController { private final MeasurementDataProvider measurementDataProvider; private final MeasurementDataReaderFactory measurementDataReaderFactory; + private final int downloadBufferSize; private static final Logger log = getLogger(DownloadController.class); public DownloadController( @Qualifier("measurementDataProvider") MeasurementDataProvider measurementDataProvider, - @Qualifier("measurementDataReaderFactory") MeasurementDataReaderFactory measurementDataReaderFactory - ) { + @Qualifier("measurementDataReaderFactory") MeasurementDataReaderFactory measurementDataReaderFactory, + @Value("${server.memory.download.buffer}") Integer downloadBufferSize) { this.measurementDataProvider = measurementDataProvider; this.measurementDataReaderFactory = measurementDataReaderFactory; + this.downloadBufferSize = Optional.ofNullable(downloadBufferSize) + .orElse(BufferedZippingFunctions.DEFAULT_BUFFER_SIZE); } @@ -107,7 +113,7 @@ private void writeDataToStream(MeasurementId measurementId, OutputStream outputS new FileTimes(file.fileInfo().registrationMillis(), -1, file.fileInfo().lastModifiedMillis())); - BufferedZippingFunctions.addToZip(zippedStream, zipEntryFileInfo, file.inputStream(), BufferedZippingFunctions.DEFAULT_BUFFER_SIZE); + BufferedZippingFunctions.addToZip(zippedStream, zipEntryFileInfo, file.inputStream(), downloadBufferSize); } } catch (IOException e) { throw new GlobalException( diff --git a/rest-api/src/main/java/life/qbic/data_download/rest/security/QBiCTokenAuthenticationProvider.java b/rest-api/src/main/java/life/qbic/data_download/rest/security/QBiCTokenAuthenticationProvider.java index 8e910e6..94fe1e1 100644 --- a/rest-api/src/main/java/life/qbic/data_download/rest/security/QBiCTokenAuthenticationProvider.java +++ b/rest-api/src/main/java/life/qbic/data_download/rest/security/QBiCTokenAuthenticationProvider.java @@ -12,6 +12,7 @@ import org.springframework.security.authentication.InternalAuthenticationServiceException; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; +import org.springframework.transaction.annotation.Transactional; /** * An {@link AuthenticationProvider} capable of authenticating a diff --git a/rest-api/src/main/java/life/qbic/data_download/rest/security/acl/setup-acl.sql b/rest-api/src/main/java/life/qbic/data_download/rest/security/acl/setup-acl.sql new file mode 100644 index 0000000..06db7bd --- /dev/null +++ b/rest-api/src/main/java/life/qbic/data_download/rest/security/acl/setup-acl.sql @@ -0,0 +1,47 @@ +CREATE TABLE acl_sid +( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + principal BOOLEAN NOT NULL, + sid VARCHAR(100) NOT NULL, + UNIQUE KEY unique_acl_sid (sid, principal) +) ENGINE = InnoDB; + +CREATE TABLE acl_class +( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + class VARCHAR(100) NOT NULL, + class_id_type VARCHAR(100) NOT NULL, + UNIQUE KEY uk_acl_class (class) +) ENGINE = InnoDB; + +CREATE TABLE acl_object_identity +( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + object_id_class BIGINT UNSIGNED NOT NULL, + object_id_identity VARCHAR(36) NOT NULL, + parent_object BIGINT UNSIGNED, + owner_sid BIGINT UNSIGNED, + entries_inheriting BOOLEAN NOT NULL, + UNIQUE KEY uk_acl_object_identity (object_id_class, object_id_identity), + CONSTRAINT fk_acl_object_identity_parent FOREIGN KEY (parent_object) REFERENCES acl_object_identity (id), + CONSTRAINT fk_acl_object_identity_class FOREIGN KEY (object_id_class) REFERENCES acl_class (id), + CONSTRAINT fk_acl_object_identity_owner FOREIGN KEY (owner_sid) REFERENCES acl_sid (id) +) ENGINE = InnoDB; + +CREATE TABLE acl_entry +( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + acl_object_identity BIGINT UNSIGNED NOT NULL, + ace_order INTEGER NOT NULL, + sid BIGINT UNSIGNED NOT NULL, + mask INTEGER UNSIGNED NOT NULL, + granting BOOLEAN NOT NULL, + audit_success BOOLEAN NOT NULL DEFAULT true, + audit_failure BOOLEAN NOT NULL, + UNIQUE KEY unique_acl_entry (acl_object_identity, ace_order), + CONSTRAINT fk_acl_entry_object FOREIGN KEY (acl_object_identity) REFERENCES acl_object_identity (id), + CONSTRAINT fk_acl_entry_acl FOREIGN KEY (sid) REFERENCES acl_sid (id) ON DELETE CASCADE +) ENGINE = InnoDB; + +INSERT INTO acl_class(id, class, class_id_type) +VALUES (1, 'life.qbic.projectmanagement.domain.model.project.Project', 'java.lang.String'); diff --git a/rest-api/src/main/java/life/qbic/data_download/rest/security/jpa/setup-roles.sql b/rest-api/src/main/java/life/qbic/data_download/rest/security/jpa/setup-roles.sql new file mode 100644 index 0000000..c899ad1 --- /dev/null +++ b/rest-api/src/main/java/life/qbic/data_download/rest/security/jpa/setup-roles.sql @@ -0,0 +1,5 @@ +INSERT INTO roles(id, name, description) +VALUES (1, 'ADMIN', 'Full administration of the application'), + (2, 'USER', 'Standard user of the application'), + (3, 'PROJECT_MANAGER', 'Manages projects at QBiC') +; diff --git a/rest-api/src/main/java/life/qbic/data_download/rest/security/jpa/user/QBiCUserDetails.java b/rest-api/src/main/java/life/qbic/data_download/rest/security/jpa/user/QBiCUserDetails.java index 0a4d8ee..0a8dcb9 100644 --- a/rest-api/src/main/java/life/qbic/data_download/rest/security/jpa/user/QBiCUserDetails.java +++ b/rest-api/src/main/java/life/qbic/data_download/rest/security/jpa/user/QBiCUserDetails.java @@ -2,11 +2,16 @@ import jakarta.persistence.Column; import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; import jakarta.persistence.Id; +import jakarta.persistence.JoinTable; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; import jakarta.persistence.Table; import java.util.Collection; import java.util.List; import java.util.Objects; +import java.util.Set; import org.springframework.data.annotation.ReadOnlyProperty; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; @@ -20,25 +25,25 @@ public class QBiCUserDetails implements UserDetails { @Id - @ReadOnlyProperty @Column(name = "id") + @ReadOnlyProperty private String id; + @OneToMany(fetch = FetchType.EAGER, mappedBy = "user") @ReadOnlyProperty + private Set userRoles; + @Column(nullable = false, name = "active") + @ReadOnlyProperty private boolean active; public String id() { return id; } - public boolean active() { - return active; - } - @Override public Collection getAuthorities() { - return List.of(); + return userRoles.stream().map(UserRole::role).toList(); } @Override diff --git a/rest-api/src/main/java/life/qbic/data_download/rest/security/jpa/user/Role.java b/rest-api/src/main/java/life/qbic/data_download/rest/security/jpa/user/Role.java new file mode 100644 index 0000000..d9d94fc --- /dev/null +++ b/rest-api/src/main/java/life/qbic/data_download/rest/security/jpa/user/Role.java @@ -0,0 +1,85 @@ +package life.qbic.data_download.rest.security.jpa.user; + +import static java.util.Objects.requireNonNull; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.util.Optional; +import java.util.StringJoiner; +import org.springframework.data.annotation.ReadOnlyProperty; +import org.springframework.security.core.GrantedAuthority; + +@Entity +@Table(name = "roles") +public class Role implements GrantedAuthority { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + @ReadOnlyProperty + private long id; + + @Column(name = "name") + @ReadOnlyProperty + private String name; + + @Column(name = "description") + @ReadOnlyProperty + private String description; + + + protected Role() { + } + + protected Role(long id, String name, String description) { + this.id = id; + this.name = name; + this.description = description; + } + + public String name() { + requireNonNull(this.name); + return name; + } + + public Optional description() { + return Optional.ofNullable(description); + } + + @Override + public String toString() { + return new StringJoiner(", ", Role.class.getSimpleName() + "[", "]") + .add("id='" + id + "'") + .add("name='" + name + "'") + .add("description='" + description + "'") + .toString(); + } + + @Override + public String getAuthority() { + return "ROLE_" + name(); + } + + public long getId() { + return id; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } else { + return obj instanceof GrantedAuthority && this.getAuthority() + .equals(((GrantedAuthority) obj).getAuthority()); + } + } + + @Override + public int hashCode() { + return getAuthority().hashCode(); + } +} diff --git a/rest-api/src/main/java/life/qbic/data_download/rest/security/jpa/user/UserDetailsRepository.java b/rest-api/src/main/java/life/qbic/data_download/rest/security/jpa/user/UserDetailsRepository.java index c6caccd..8e9565b 100644 --- a/rest-api/src/main/java/life/qbic/data_download/rest/security/jpa/user/UserDetailsRepository.java +++ b/rest-api/src/main/java/life/qbic/data_download/rest/security/jpa/user/UserDetailsRepository.java @@ -2,7 +2,8 @@ import java.util.Optional; import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.Repository; -public interface UserDetailsRepository extends CrudRepository { +public interface UserDetailsRepository extends Repository { Optional findById(String id); } diff --git a/rest-api/src/main/java/life/qbic/data_download/rest/security/jpa/user/UserRole.java b/rest-api/src/main/java/life/qbic/data_download/rest/security/jpa/user/UserRole.java new file mode 100644 index 0000000..89bb1ce --- /dev/null +++ b/rest-api/src/main/java/life/qbic/data_download/rest/security/jpa/user/UserRole.java @@ -0,0 +1,37 @@ +package life.qbic.data_download.rest.security.jpa.user; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import java.io.Serializable; +import org.springframework.data.annotation.ReadOnlyProperty; + +@Entity +@Table(name = "user_role") +public class UserRole implements Serializable { + @Id + @Column(name = "id") + @GeneratedValue(strategy = GenerationType.IDENTITY) + @ReadOnlyProperty + private long id; + + @ManyToOne(optional = false) + @JoinColumn(name = "userId", nullable = false) + @ReadOnlyProperty + private QBiCUserDetails user; + + @ManyToOne(optional = false) + @JoinColumn(name = "roleId") + @ReadOnlyProperty + private Role role; + + public Role role() { + return role; + } +} diff --git a/rest-api/src/main/resources/application.properties b/rest-api/src/main/resources/application.properties index 39eb20b..054d2dd 100644 --- a/rest-api/src/main/resources/application.properties +++ b/rest-api/src/main/resources/application.properties @@ -16,6 +16,7 @@ qbic.access-token.iteration-count=${ACCESS_TOKEN_ITERATIONS:100000} ### server settings server.download.token-name=${TOKEN_NAME:Bearer} +server.memory.download.buffer=${DOWNLOAD_BUFFER:} server.port=${SERVER_PORT:8090} server.servlet.context-path=${SERVER_CONTEXT_PATH:}