Skip to content

Commit

Permalink
feat: support for enabling anonymous access for s3 urls
Browse files Browse the repository at this point in the history
By setting env variable REMOTEFILES_S3_ANONYMOUSACCESS to true, s3 urls will be
accessed in anonymous mode, corresponding to using the '--no-sign-request' flag
with the aws cli. Any s3 access key or secrets key configured will be
ignored.

Signed-off-by: Gustav Grusell <[email protected]>
  • Loading branch information
grusell committed Dec 17, 2024
1 parent 37ccec9 commit 568e1ad
Show file tree
Hide file tree
Showing 6 changed files with 142 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import se.svt.oss.encore.service.remotefiles.s3.S3Properties
import se.svt.oss.encore.service.remotefiles.s3.S3RemoteFileHandler
import se.svt.oss.encore.service.remotefiles.s3.S3UriConverter
import software.amazon.awssdk.auth.credentials.AnonymousCredentialsProvider
import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider
import software.amazon.awssdk.regions.Region
import software.amazon.awssdk.services.s3.S3AsyncClient
import software.amazon.awssdk.services.s3.S3Configuration
Expand All @@ -35,6 +38,13 @@ class S3RemoteFilesConfiguration {
.pathStyleAccessEnabled(true)
.build()
)
.credentialsProvider(
if (s3Properties.anonymousAccess) {
AnonymousCredentialsProvider.create()
} else {
DefaultCredentialsProvider.create()
}
)
.apply {
if (!s3Properties.endpoint.isNullOrBlank()) {
endpointOverride(URI.create(s3Properties.endpoint))
Expand All @@ -58,6 +68,9 @@ class S3RemoteFilesConfiguration {
.build()

@Bean
fun s3RemoteFileHandler(s3Client: S3AsyncClient, s3Presigner: S3Presigner, s3Properties: S3Properties) =
S3RemoteFileHandler(s3Client, s3Presigner, s3Properties)
fun s3UriConverter(s3Properties: S3Properties, s3Region: Region) = S3UriConverter(s3Properties, s3Region)

@Bean
fun s3RemoteFileHandler(s3Client: S3AsyncClient, s3Presigner: S3Presigner, s3Properties: S3Properties, s3UriConverter: S3UriConverter) =
S3RemoteFileHandler(s3Client, s3Presigner, s3Properties, s3UriConverter)
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@ import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonSubTypes
import com.fasterxml.jackson.annotation.JsonTypeInfo
import io.swagger.v3.oas.annotations.media.Schema
import jakarta.validation.constraints.Pattern
import jakarta.validation.constraints.PositiveOrZero
import se.svt.oss.encore.model.mediafile.toParams
import se.svt.oss.encore.model.profile.ChannelLayout
import se.svt.oss.mediaanalyzer.file.FractionString
import se.svt.oss.mediaanalyzer.file.MediaContainer
import se.svt.oss.mediaanalyzer.file.MediaFile
import se.svt.oss.mediaanalyzer.file.VideoFile
import jakarta.validation.constraints.Pattern
import jakarta.validation.constraints.PositiveOrZero

const val TYPE_AUDIO_VIDEO = "AudioVideo"
const val TYPE_AUDIO = "Audio"
Expand Down Expand Up @@ -255,7 +255,7 @@ fun List<Input>.inputParams(readDuration: Double?): List<String> =
(readDuration?.let { listOf("-t", "$it") } ?: emptyList()) +
(input.seekTo?.let { listOf("-ss", "$it") } ?: emptyList()) +
(if (input.copyTs) listOf("-copyts") else emptyList()) +
listOf("-i", input.accessUri ?: input.uri)
listOf("-i", input.accessUri)
}

fun List<Input>.maxDuration(): Double? = maxOfOrNull {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import java.time.Duration
@ConfigurationProperties("remote-files.s3")
data class S3Properties(
val enabled: Boolean = false,
val anonymousAccess: Boolean = false,
val endpoint: String = "",
val presignDurationSeconds: Long = Duration.ofHours(12).seconds,
val uploadTimeoutSeconds: Long = Duration.ofHours(1).seconds
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,27 @@ import java.util.concurrent.TimeUnit
class S3RemoteFileHandler(
private val client: S3AsyncClient,
private val presigner: S3Presigner,
private val s3Properties: S3Properties
private val s3Properties: S3Properties,
private val s3UriConverter: S3UriConverter
) : RemoteFileHandler {

private val log = mu.KotlinLogging.logger {}

override fun getAccessUri(uri: String): String {
val s3Uri = URI.create(uri)

if (s3Properties.anonymousAccess) {
return s3UriConverter.toHttp(s3Uri)
}
return presignUrl(s3Uri)
}

private fun presignUrl(s3Uri: URI): String {
val (bucket, key) = s3UriConverter.getBucketAndKey(s3Uri)

val objectRequest: GetObjectRequest = GetObjectRequest.builder()
.bucket(s3Uri.host)
.key(s3Uri.path.stripLeadingSlash())
.bucket(bucket)
.key(key)
.build()
val presignRequest: GetObjectPresignRequest = GetObjectPresignRequest.builder()
.signatureDuration(java.time.Duration.ofSeconds(s3Properties.presignDurationSeconds))
Expand All @@ -42,17 +52,14 @@ class S3RemoteFileHandler(
override fun upload(localFile: String, remoteFile: String) {
log.info { "Uploading $localFile to $remoteFile" }
val s3Uri = URI.create(remoteFile)
val bucket = s3Uri.host
val objectName = s3Uri.path.stripLeadingSlash()
val (bucket, key) = s3UriConverter.getBucketAndKey(s3Uri)
val putObjectRequest: PutObjectRequest = PutObjectRequest.builder()
.bucket(bucket)
.key(objectName)
.key(key)
.build()
val res = client.putObject(putObjectRequest, Paths.get(localFile)).get(s3Properties.presignDurationSeconds, TimeUnit.SECONDS)
log.info { "Upload result: $res" }
}

private fun String.stripLeadingSlash() = if (startsWith("/")) substring(1) else this

override val protocols = listOf("s3")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// SPDX-FileCopyrightText: 2024 Eyevinn Technology AB
//
// SPDX-License-Identifier: EUPL-1.2

package se.svt.oss.encore.service.remotefiles.s3

import software.amazon.awssdk.regions.Region
import java.net.URI

class S3UriConverter(
private val s3Properties: S3Properties,
private val region: Region
) {

fun toHttp(s3Uri: URI): String {
if (s3Uri.scheme != "s3") {
throw IllegalArgumentException("Invalid URI: $s3Uri")
}
val bucket = s3Uri.host
val key = s3Uri.path.stripLeadingSlash()

if (s3Properties.endpoint.isNotBlank()) {
return "https://$bucket.${s3Properties.endpoint}/$key"
}
return "https://$bucket.s3.$region.amazonaws.com/$key"
}

fun getBucketAndKey(s3Uri: URI): Pair<String, String> {
if (s3Uri.scheme != "s3") {
throw IllegalArgumentException("Invalid URI: $s3Uri")
}
val bucket = s3Uri.host
val key = s3Uri.path.stripLeadingSlash()
return Pair(bucket, key)
}

private fun String.stripLeadingSlash() = if (startsWith("/")) substring(1) else this
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// SPDX-FileCopyrightText: 2024 Eyevinn Technology AB
//
// SPDX-License-Identifier: EUPL-1.2

package se.svt.oss.encore.service.remotefiles

import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.jupiter.api.Test
import se.svt.oss.encore.service.remotefiles.s3.S3Properties
import se.svt.oss.encore.service.remotefiles.s3.S3UriConverter
import software.amazon.awssdk.regions.Region
import java.net.URI

class S3UriConverterTest {

private val s3Properties = S3Properties(enabled = true, anonymousAccess = true)
private val region = Region.of("eu-west-1")
private val s3Uri = URI.create("s3://my-bucket/test2/test1_x264_3100.mp4")
private val s3UriConverter = S3UriConverter(s3Properties, region)

@Test
fun toHttpReturnsCorrectUri() {
val httpUri = s3UriConverter.toHttp(s3Uri)
assertThat(httpUri)
.isEqualTo("https://my-bucket.s3.eu-west-1.amazonaws.com/test2/test1_x264_3100.mp4")
}

@Test
fun differentRegionReturnsCorrectUri() {
val s3UriConverter = S3UriConverter(s3Properties, Region.of("eu-north-1"))

val httpUri = s3UriConverter.toHttp(s3Uri)
assertThat(httpUri)
.isEqualTo("https://my-bucket.s3.eu-north-1.amazonaws.com/test2/test1_x264_3100.mp4")
}

@Test
fun toHttpWithCustomEndpointReturnsCorrectUri() {
val endpoint = "some-host:1234"
val s3UriConverter = S3UriConverter(s3Properties.copy(endpoint = endpoint), region)

val httpUri = s3UriConverter.toHttp(s3Uri)
assertThat(httpUri)
.isEqualTo("https://my-bucket.some-host:1234/test2/test1_x264_3100.mp4")
}

@Test
fun getBucketAndKeyReturnsCorrectValues() {
val (bucket, key) = s3UriConverter.getBucketAndKey(s3Uri)
assertThat(bucket).isEqualTo("my-bucket")
assertThat(key).isEqualTo("test2/test1_x264_3100.mp4")
}

@Test
fun toHttpNonS3UriThrowsException() {
val uri = URI.create("https://my-bucket/test2/test1_x264_3100.mp4")
assertThatThrownBy { s3UriConverter.toHttp(uri) }
.isInstanceOf(IllegalArgumentException::class.java)
.hasMessage("Invalid URI: $uri")
}

@Test
fun getBucketAndKeyNonS3UriThrowsException() {
val uri = URI.create("https://my-bucket/test2/test1_x264_3100.mp4")
assertThatThrownBy { s3UriConverter.getBucketAndKey(uri) }
.isInstanceOf(IllegalArgumentException::class.java)
.hasMessage("Invalid URI: $uri")
}
}

0 comments on commit 568e1ad

Please sign in to comment.