Skip to content

Commit

Permalink
Impl. file upload endpoint
Browse files Browse the repository at this point in the history
Currently only implemented for S3 storage type.
  • Loading branch information
pvannierop committed Jun 3, 2024
1 parent b722d22 commit 4fc5de2
Show file tree
Hide file tree
Showing 12 changed files with 216 additions and 2 deletions.
2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ ext {
radarSpringAuthVersion = '1.2.1'
springSecurityVersion = '6.0.2'
hibernateValidatorVersion = '8.0.0.Final'
minioVersion = '8.5.10'
}

sourceSets {
Expand All @@ -64,6 +65,7 @@ dependencies {
implementation('org.springframework.security.oauth.boot:spring-security-oauth2-autoconfigure:' + springBootVersion)
implementation('org.springframework.security.oauth:spring-security-oauth2:' + springOauth2Version)
runtimeOnly("org.hibernate.validator:hibernate-validator:$hibernateValidatorVersion")
implementation("io.minio:minio:$minioVersion")

// Open API spec
implementation(group: 'org.springdoc', name: 'springdoc-openapi-starter-webmvc-ui', version: springDocVersion)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@

@Configuration
@EnableJpaAuditing
@EnableConfigurationProperties({FcmServerConfig.class})
@EnableConfigurationProperties({FcmServerConfig.class, S3StorageProperties.class})
@EnableTransactionManagement
@EnableAsync
@EnableScheduling
Expand Down
14 changes: 14 additions & 0 deletions src/main/java/org/radarbase/appserver/config/AuthConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.security.access.PermissionEvaluator;
import radar.spring.auth.common.AuthAspect;
import radar.spring.auth.common.AuthValidator;
import radar.spring.auth.common.Authorization;
Expand All @@ -27,6 +28,11 @@ public class AuthConfig {
@Value("${security.oauth2.resource.id}")
private transient String resourceName;

@Bean
public PermissionEvaluator getPermissionEvaluator() {
return new PreAuthPermissionEvaluator();
}

@Bean
public ManagementPortalAuthProperties getAuthProperties() {
TokenVerifierPublicKeyConfig validatorConfig = TokenVerifierPublicKeyConfig.readFromFileOrClasspath();
Expand Down Expand Up @@ -69,5 +75,13 @@ public interface AuthPermissions {
String READ = "READ";
String CREATE = "CREATE";
String UPDATE = "UPDATE";
String UPLOAD = "UPLOAD";
}

public enum AuthPermission {
READ,
CREATE,
UPDATE,
UPLOAD
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package org.radarbase.appserver.config;

import org.radarbase.appserver.config.AuthConfig.AuthPermission;
import org.springframework.security.access.PermissionEvaluator;
import org.springframework.security.core.Authentication;

import java.io.Serializable;

public class PreAuthPermissionEvaluator implements PermissionEvaluator {

@Override
public boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission) {
if (targetDomainObject instanceof String && permission instanceof AuthPermission) {
String subjectId = (String) targetDomainObject;
AuthPermission authPermission = (AuthPermission) permission;
if (AuthPermission.UPLOAD == authPermission) {
// TODO check lookup of subjectId in authentication object.
if (authentication.getName().equals(subjectId)) {
return true;
}
}
}
return false;
}

@Override
public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) {
return false;
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package org.radarbase.appserver.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;

@Data
@ConfigurationProperties("storage.s3")
public class S3StorageProperties {
private String url;
private String accessKey;
private String secretKey;
private String bucketName;
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,13 @@ public class PathsUtil {

public static final String USER_PATH = "users";
public static final String PROJECT_PATH = "projects";
public static final String TOPIC_PATH = "topic";
public static final String MESSAGING_NOTIFICATION_PATH = "messaging/notifications";
public static final String MESSAGING_DATA_PATH = "messaging/data";
public static final String PROTOCOL_PATH = "protocols";
public static final String PROJECT_ID_CONSTANT = "{projectId}";
public static final String SUBJECT_ID_CONSTANT = "{subjectId}";
public static final String TOPIC_ID_CONSTANT = "{topicId}";
public static final String NOTIFICATION_ID_CONSTANT = "{notificationId}";
public static final String NOTIFICATION_STATE_EVENTS_PATH = "state_events";
public static final String QUESTIONNAIRE_SCHEDULE_PATH = "questionnaire/schedule";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package org.radarbase.appserver.controller;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j;
import org.radarbase.appserver.config.AuthConfig.AuthEntities;
import org.radarbase.appserver.config.AuthConfig.AuthPermissions;
import org.radarbase.appserver.dto.FilePathDto;
import org.radarbase.appserver.service.StorageService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import radar.spring.auth.common.Authorized;

/**
* Resource Endpoint for uploading assets to a data store.
*
* @author Pim van Nierop
*/
@Slf4j
@CrossOrigin
@RestController
@ConditionalOnProperty(value = "file-upload.enabled", havingValue = "true")
public class UploadController {

@Autowired
private StorageService storageService;

@Authorized(permission = AuthPermissions.CREATE, entity = AuthEntities.MEASUREMENT)
@PostMapping(
"/" + PathsUtil.PROJECT_PATH + "/" + PathsUtil.PROJECT_ID_CONSTANT +
"/" + PathsUtil.USER_PATH + "/" + PathsUtil.SUBJECT_ID_CONSTANT +
"/" + PathsUtil.TOPIC_PATH + "/" + PathsUtil.TOPIC_ID_CONSTANT +
"/upload")
@PreAuthorize("hasPermission(#subjectId, T(org.radarbase.appserver.config.AuthConfig.AuthPermission).UPLOAD") // Only the user can upload files on their own behalf.
public ResponseEntity<FilePathDto> subjectFileUpload(
@RequestParam("file") MultipartFile file,
@PathVariable String projectId,
@PathVariable String subjectId,
@PathVariable String topicId) {
log.info("Storing file for project: {}, subject: {}, topic: {}", projectId, subjectId, topicId);
String filePath = storageService.store(file, projectId, subjectId, topicId);
return ResponseEntity.ok(new FilePathDto(filePath));
}
}
12 changes: 12 additions & 0 deletions src/main/java/org/radarbase/appserver/dto/FilePathDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package org.radarbase.appserver.dto;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class FilePathDto {
private String path;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package org.radarbase.appserver.service;

import io.minio.BucketExistsArgs;
import io.minio.MinioClient;
import io.minio.PutObjectArgs;
import lombok.extern.slf4j.Slf4j;
import org.radarbase.appserver.config.S3StorageProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.PostConstruct;

@Slf4j
@Service
@ConditionalOnExpression("${file-upload.enabled:false} and 's3' == '${storage.type:}'")
public class S3StorageService implements StorageService {

@Autowired
private S3StorageProperties s3StorageProperties;

private MinioClient minioClient;
private String bucketName;

@PostConstruct
public void init() {
try {
minioClient =
MinioClient.builder()
.endpoint(s3StorageProperties.getUrl())
.credentials(s3StorageProperties.getAccessKey(), s3StorageProperties.getSecretKey())
.build();
bucketName = s3StorageProperties.getBucketName();
boolean found =
minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
if (!found) {
throw new RuntimeException(String.format("S3 bucket '%s' does not exist", bucketName));
}
} catch (Exception e) {
throw new RuntimeException("Could not connect to S3", e);
}
}

public String store(MultipartFile file, String projectId, String subjectId, String topicId) {
String filePath = String.format("%s/%s/%s/%s", projectId, subjectId, topicId, file.getName());
log.debug("Storing file at path: {}", filePath);
try {
minioClient.putObject(PutObjectArgs
.builder()
.bucket(bucketName)
.object(filePath)
.stream(file.getInputStream(), file.getSize(), -1)
.build());
} catch (Exception e) {
throw new RuntimeException("Could not store file", e);
}
return filePath;
}
}
18 changes: 18 additions & 0 deletions src/main/java/org/radarbase/appserver/service/StorageService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package org.radarbase.appserver.service;

import io.minio.errors.ErrorResponseException;
import io.minio.errors.InsufficientDataException;
import io.minio.errors.InternalException;
import io.minio.errors.InvalidResponseException;
import io.minio.errors.ServerException;
import io.minio.errors.XmlParserException;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;

public interface StorageService {
String store(MultipartFile file, String projectId, String subjectId, String topicId);
}
8 changes: 8 additions & 0 deletions src/main/resources/application-prod.properties
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,11 @@ security.github.client.maxContentLength=1000000
security.github.cache.size=10000
security.github.cache.duration=3600
security.github.cache.retryDuration=60

# DATA UPLOAD
file-upload.enabled=true
storage.type=s3 # can be 's3'
storage.s3.url=http://localhost:9000
storage.s3.access-key=access-key
storage.s3.secret-key=secret-key
storage.s3.bucket-name=radar
2 changes: 1 addition & 1 deletion src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,4 @@
# *
# */
#
spring.profiles.active=prod
spring.profiles.active=prod

0 comments on commit 4fc5de2

Please sign in to comment.