Skip to content

Commit 2c335ec

Browse files
authored
Merge pull request #1327 from achdmbp/feature/aws-irsa-support
feat: Add IRSA and session token support to AWS Storage Service
2 parents 3a90532 + a85c501 commit 2c335ec

File tree

5 files changed

+947
-10
lines changed

5 files changed

+947
-10
lines changed

README.md

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,10 +154,46 @@ If you would like to test file storage via Amazon S3, follow these steps:
154154
}
155155
]
156156
```
157+
158+
#### Authentication Methods
159+
160+
OpenVSX supports multiple AWS authentication methods with the following precedence:
161+
162+
1. **Static credentials with session token** (temporary credentials)
163+
2. **Static credentials without session token** (permanent credentials)
164+
3. **IAM role-based credentials** (using AWS Web Identity Token authentication)
165+
4. **Default credential provider chain** (fallback for other AWS credential sources)
166+
167+
#### Option 1: Static Credentials (Traditional)
168+
157169
* 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
158-
* Configure the following environment variables on your server environment
170+
* Configure the following environment variables:
159171
* `AWS_ACCESS_KEY_ID` with your access key id
160172
* `AWS_SECRET_ACCESS_KEY` with your secret access key
173+
* `AWS_SESSION_TOKEN` with your session token (optional, for temporary credentials)
174+
175+
#### Option 2: IAM Role with Web Identity Token (Recommended for containerized deployments)
176+
177+
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):
178+
179+
* Create an IAM role with S3 permissions and appropriate trust policy
180+
* Configure your deployment environment to provide the following environment variables:
181+
* `AWS_ROLE_ARN` - The ARN of the IAM role to assume
182+
* `AWS_WEB_IDENTITY_TOKEN_FILE` - Path to the web identity token file
183+
* No static credentials needed!
184+
185+
#### Option 3: Default Credential Provider Chain
186+
187+
OpenVSX will automatically detect credentials from:
188+
* Environment variables (`AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`)
189+
* AWS credentials file (`~/.aws/credentials`)
190+
* AWS config file (`~/.aws/config`)
191+
* IAM instance profile (for EC2 instances)
192+
* Container credentials (for ECS tasks)
193+
194+
#### Common Configuration
195+
196+
Regardless of authentication method, configure these environment variables:
161197
* `AWS_REGION` with your bucket region name
162198
* `AWS_SERVICE_ENDPOINT` with the url of your S3 provider if not using AWS (for AWS do not set)
163199
* `AWS_BUCKET` with your bucket name

server/build.gradle

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ def versions = [
2929
azure: '12.23.0',
3030
aws: '2.29.29',
3131
junit: '5.9.2',
32-
testcontainers: '1.15.2',
32+
testcontainers: '1.19.3',
3333
jackson: '2.15.2',
3434
woodstox: '6.4.0',
3535
jobrunr: '7.5.0',
@@ -101,6 +101,7 @@ dependencies {
101101
implementation "com.google.cloud:google-cloud-storage:${versions.gcloud}"
102102
implementation "com.azure:azure-storage-blob:${versions.azure}"
103103
implementation "software.amazon.awssdk:s3:${versions.aws}"
104+
implementation "software.amazon.awssdk:sts:${versions.aws}"
104105
implementation "org.springdoc:springdoc-openapi-starter-webmvc-ui:${versions.springdoc}"
105106
implementation "com.fasterxml.jackson.core:jackson-core:${versions.jackson}"
106107
implementation "com.fasterxml.jackson.core:jackson-annotations:${versions.jackson}"
@@ -128,10 +129,12 @@ dependencies {
128129
}
129130
testImplementation "org.springframework.security:spring-security-test"
130131
testImplementation "org.testcontainers:elasticsearch:${versions.testcontainers}"
132+
testImplementation "org.testcontainers:localstack:${versions.testcontainers}"
133+
testImplementation "org.testcontainers:junit-jupiter:${versions.testcontainers}"
131134
testImplementation "org.junit.jupiter:junit-jupiter-api:${versions.junit}"
132135
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:${versions.junit}"
133136
testRuntimeOnly "org.testcontainers:postgresql:${versions.testcontainers}"
134-
137+
135138
gatling "io.gatling:gatling-core:${versions.gatling}"
136139
gatling "io.gatling:gatling-app:${versions.gatling}"
137140
}
@@ -198,8 +201,23 @@ task unitTests(type: Test) {
198201
exclude 'org/eclipse/openvsx/IntegrationTest.class'
199202
exclude 'org/eclipse/openvsx/cache/CacheServiceTest.class'
200203
exclude 'org/eclipse/openvsx/repositories/RepositoryServiceSmokeTest.class'
204+
exclude 'org/eclipse/openvsx/storage/AwsStorageServiceIntegrationTest.class'
201205
}
202206

207+
task s3IntegrationTests(type: Test) {
208+
description = 'Runs S3 integration tests using LocalStack (requires Docker/Podman).'
209+
group = 'verification'
210+
testClassesDirs = sourceSets.test.output.classesDirs
211+
classpath = sourceSets.test.runtimeClasspath
212+
useJUnitPlatform()
213+
include 'org/eclipse/openvsx/storage/AwsStorageServiceIntegrationTest.class'
214+
215+
// Set system properties for test configuration
216+
systemProperty 'spring.profiles.active', 's3-integration'
217+
}
218+
219+
220+
203221
jacocoTestReport {
204222
reports {
205223
xml.required = true

server/src/main/java/org/eclipse/openvsx/storage/AwsStorageService.java

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@
2323
import org.springframework.data.util.Pair;
2424
import org.springframework.stereotype.Component;
2525
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
26+
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
27+
import software.amazon.awssdk.auth.credentials.AwsSessionCredentials;
28+
import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider;
2629
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
2730
import software.amazon.awssdk.awscore.defaultsmode.DefaultsMode;
2831
import software.amazon.awssdk.regions.Region;
@@ -60,6 +63,9 @@ public class AwsStorageService implements IStorageService {
6063
@Value("${ovsx.storage.aws.secret-access-key:}")
6164
String secretAccessKey;
6265

66+
@Value("${ovsx.storage.aws.session-token:}")
67+
String sessionToken;
68+
6369
@Value("${ovsx.storage.aws.region:}")
6470
String region;
6571

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

8288
protected S3Client getS3Client() {
8389
if (s3Client == null) {
84-
var credentials = AwsBasicCredentials.create(accessKeyId, secretAccessKey);
8590
var s3ClientBuilder = S3Client.builder()
8691
.defaultsMode(DefaultsMode.STANDARD)
8792
.forcePathStyle(pathStyleAccess)
88-
.credentialsProvider(StaticCredentialsProvider.create(credentials))
89-
.region(Region.of(region));
93+
.region(Region.of(region))
94+
.credentialsProvider(getCredentialsProvider());
9095

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

109114
private S3Presigner getS3Presigner() {
110-
var credentials = AwsBasicCredentials.create(accessKeyId, secretAccessKey);
111115
var builder = S3Presigner.builder()
112-
.credentialsProvider(StaticCredentialsProvider.create(credentials))
113-
.region(Region.of(region));
116+
.region(Region.of(region))
117+
.credentialsProvider(getCredentialsProvider());
114118

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

135+
private AwsCredentialsProvider getCredentialsProvider() {
136+
// Use static credentials if provided, otherwise DefaultCredentialsProvider handles everything
137+
if (hasStaticCredentials()) {
138+
var credentials = hasSessionToken()
139+
? AwsSessionCredentials.create(accessKeyId, secretAccessKey, sessionToken)
140+
: AwsBasicCredentials.create(accessKeyId, secretAccessKey);
141+
return StaticCredentialsProvider.create(credentials);
142+
}
143+
return DefaultCredentialsProvider.create();
144+
}
145+
146+
147+
private boolean hasStaticCredentials() {
148+
return !StringUtils.isEmpty(accessKeyId) && !StringUtils.isEmpty(secretAccessKey);
149+
}
150+
151+
private boolean hasSessionToken() {
152+
return !StringUtils.isEmpty(sessionToken);
153+
}
131154
@Override
132155
public boolean isEnabled() {
133-
return !StringUtils.isEmpty(accessKeyId);
156+
// Require region and bucket to be configured
157+
if (StringUtils.isEmpty(region) || StringUtils.isEmpty(bucket)) {
158+
return false;
159+
}
160+
161+
// If any credential fields are provided, validate them properly
162+
boolean hasAccessKey = !StringUtils.isEmpty(accessKeyId);
163+
boolean hasSecretKey = !StringUtils.isEmpty(secretAccessKey);
164+
boolean hasSessionToken = !StringUtils.isEmpty(sessionToken);
165+
166+
if (hasAccessKey || hasSecretKey || hasSessionToken) {
167+
// If any credential is provided, both access key and secret key must be present
168+
return hasAccessKey && hasSecretKey;
169+
}
170+
171+
// No static credentials provided - allow AWS default credential provider chain
172+
return true;
134173
}
135174

136175
@Override

0 commit comments

Comments
 (0)