Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 37 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,10 +154,46 @@ If you would like to test file storage via Amazon S3, follow these steps:
}
]
```

#### Authentication Methods

OpenVSX supports multiple AWS authentication methods with the following precedence:

1. **Static credentials with session token** (temporary credentials)
2. **Static credentials without session token** (permanent credentials)
3. **IAM role-based credentials** (using AWS Web Identity Token authentication)
4. **Default credential provider chain** (fallback for other AWS credential sources)

#### Option 1: Static Credentials (Traditional)

* Follow the steps for [programmatic access](https://docs.aws.amazon.com/general/latest/gr/aws-sec-cred-types.html#access-keys-and-secret-access-keys) to create your access key id and secret access key
* Configure the following environment variables on your server environment
* Configure the following environment variables:
* `AWS_ACCESS_KEY_ID` with your access key id
* `AWS_SECRET_ACCESS_KEY` with your secret access key
* `AWS_SESSION_TOKEN` with your session token (optional, for temporary credentials)

#### Option 2: IAM Role with Web Identity Token (Recommended for containerized deployments)

For deployments using IAM roles with web identity token authentication (such as IRSA in Kubernetes, ECS tasks with task roles, or other container orchestration platforms):

* Create an IAM role with S3 permissions and appropriate trust policy
* Configure your deployment environment to provide the following environment variables:
* `AWS_ROLE_ARN` - The ARN of the IAM role to assume
* `AWS_WEB_IDENTITY_TOKEN_FILE` - Path to the web identity token file
* No static credentials needed!

#### Option 3: Default Credential Provider Chain

OpenVSX will automatically detect credentials from:
* Environment variables (`AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`)
* AWS credentials file (`~/.aws/credentials`)
* AWS config file (`~/.aws/config`)
* IAM instance profile (for EC2 instances)
* Container credentials (for ECS tasks)

#### Common Configuration

Regardless of authentication method, configure these environment variables:
* `AWS_REGION` with your bucket region name
* `AWS_SERVICE_ENDPOINT` with the url of your S3 provider if not using AWS (for AWS do not set)
* `AWS_BUCKET` with your bucket name
Expand Down
22 changes: 20 additions & 2 deletions server/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def versions = [
azure: '12.23.0',
aws: '2.29.29',
junit: '5.9.2',
testcontainers: '1.15.2',
testcontainers: '1.19.3',
jackson: '2.15.2',
woodstox: '6.4.0',
jobrunr: '7.5.0',
Expand Down Expand Up @@ -101,6 +101,7 @@ dependencies {
implementation "com.google.cloud:google-cloud-storage:${versions.gcloud}"
implementation "com.azure:azure-storage-blob:${versions.azure}"
implementation "software.amazon.awssdk:s3:${versions.aws}"
implementation "software.amazon.awssdk:sts:${versions.aws}"
implementation "org.springdoc:springdoc-openapi-starter-webmvc-ui:${versions.springdoc}"
implementation "com.fasterxml.jackson.core:jackson-core:${versions.jackson}"
implementation "com.fasterxml.jackson.core:jackson-annotations:${versions.jackson}"
Expand Down Expand Up @@ -128,10 +129,12 @@ dependencies {
}
testImplementation "org.springframework.security:spring-security-test"
testImplementation "org.testcontainers:elasticsearch:${versions.testcontainers}"
testImplementation "org.testcontainers:localstack:${versions.testcontainers}"
testImplementation "org.testcontainers:junit-jupiter:${versions.testcontainers}"
testImplementation "org.junit.jupiter:junit-jupiter-api:${versions.junit}"
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:${versions.junit}"
testRuntimeOnly "org.testcontainers:postgresql:${versions.testcontainers}"

gatling "io.gatling:gatling-core:${versions.gatling}"
gatling "io.gatling:gatling-app:${versions.gatling}"
}
Expand Down Expand Up @@ -198,8 +201,23 @@ task unitTests(type: Test) {
exclude 'org/eclipse/openvsx/IntegrationTest.class'
exclude 'org/eclipse/openvsx/cache/CacheServiceTest.class'
exclude 'org/eclipse/openvsx/repositories/RepositoryServiceSmokeTest.class'
exclude 'org/eclipse/openvsx/storage/AwsStorageServiceIntegrationTest.class'
}

task s3IntegrationTests(type: Test) {
description = 'Runs S3 integration tests using LocalStack (requires Docker/Podman).'
group = 'verification'
testClassesDirs = sourceSets.test.output.classesDirs
classpath = sourceSets.test.runtimeClasspath
useJUnitPlatform()
include 'org/eclipse/openvsx/storage/AwsStorageServiceIntegrationTest.class'

// Set system properties for test configuration
systemProperty 'spring.profiles.active', 's3-integration'
}



jacocoTestReport {
reports {
xml.required = true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
import org.springframework.data.util.Pair;
import org.springframework.stereotype.Component;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
import software.amazon.awssdk.auth.credentials.AwsSessionCredentials;
import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.awscore.defaultsmode.DefaultsMode;
import software.amazon.awssdk.regions.Region;
Expand Down Expand Up @@ -60,6 +63,9 @@ public class AwsStorageService implements IStorageService {
@Value("${ovsx.storage.aws.secret-access-key:}")
String secretAccessKey;

@Value("${ovsx.storage.aws.session-token:}")
String sessionToken;

@Value("${ovsx.storage.aws.region:}")
String region;

Expand All @@ -81,12 +87,11 @@ public AwsStorageService(FileCacheDurationConfig fileCacheDurationConfig, FilesC

protected S3Client getS3Client() {
if (s3Client == null) {
var credentials = AwsBasicCredentials.create(accessKeyId, secretAccessKey);
var s3ClientBuilder = S3Client.builder()
.defaultsMode(DefaultsMode.STANDARD)
.forcePathStyle(pathStyleAccess)
.credentialsProvider(StaticCredentialsProvider.create(credentials))
.region(Region.of(region));
.region(Region.of(region))
.credentialsProvider(getCredentialsProvider());

if(StringUtils.isNotEmpty(serviceEndpoint)) {
var endpointParams = S3EndpointParams.builder()
Expand All @@ -107,10 +112,9 @@ protected S3Client getS3Client() {
}

private S3Presigner getS3Presigner() {
var credentials = AwsBasicCredentials.create(accessKeyId, secretAccessKey);
var builder = S3Presigner.builder()
.credentialsProvider(StaticCredentialsProvider.create(credentials))
.region(Region.of(region));
.region(Region.of(region))
.credentialsProvider(getCredentialsProvider());

if(StringUtils.isNotEmpty(serviceEndpoint)) {
var endpointParams = S3EndpointParams.builder()
Expand All @@ -128,9 +132,44 @@ private S3Presigner getS3Presigner() {
return builder.build();
}

private AwsCredentialsProvider getCredentialsProvider() {
// Use static credentials if provided, otherwise DefaultCredentialsProvider handles everything
if (hasStaticCredentials()) {
var credentials = hasSessionToken()
? AwsSessionCredentials.create(accessKeyId, secretAccessKey, sessionToken)
: AwsBasicCredentials.create(accessKeyId, secretAccessKey);
return StaticCredentialsProvider.create(credentials);
}
return DefaultCredentialsProvider.create();
}


private boolean hasStaticCredentials() {
return !StringUtils.isEmpty(accessKeyId) && !StringUtils.isEmpty(secretAccessKey);
}

private boolean hasSessionToken() {
return !StringUtils.isEmpty(sessionToken);
}
@Override
public boolean isEnabled() {
return !StringUtils.isEmpty(accessKeyId);
// Require region and bucket to be configured
if (StringUtils.isEmpty(region) || StringUtils.isEmpty(bucket)) {
return false;
}

// If any credential fields are provided, validate them properly
boolean hasAccessKey = !StringUtils.isEmpty(accessKeyId);
boolean hasSecretKey = !StringUtils.isEmpty(secretAccessKey);
boolean hasSessionToken = !StringUtils.isEmpty(sessionToken);

if (hasAccessKey || hasSecretKey || hasSessionToken) {
// If any credential is provided, both access key and secret key must be present
return hasAccessKey && hasSecretKey;
}

// No static credentials provided - allow AWS default credential provider chain
return true;
}

@Override
Expand Down
Loading