Skip to content

Commit d375447

Browse files
committed
feat: Add IRSA and session token support to AWS Storage Service
This enhancement addresses the AWS credential limitations in OpenVSX by adding support for multiple authentication methods: 1. Static credentials with session token (temporary credentials) 2. Static credentials without session token (permanent credentials) 3. IRSA credentials (IAM Roles for Service Accounts) 4. Default credential provider chain (fallback) Key improvements: - Enables secure Kubernetes deployments using IRSA - Supports temporary credentials from AWS STS - Maintains backward compatibility with existing configurations - Follows AWS security best practices - Eliminates need for long-lived static credentials in containers The service automatically detects available credential types and uses appropriate AWS SDK credential providers based on configuration. Updated documentation includes examples for all authentication methods and deployment scenarios. Fixes: #1316 Signed-off-by: Adnan Al <[email protected]>
1 parent cfcfa7d commit d375447

File tree

5 files changed

+946
-10
lines changed

5 files changed

+946
-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: 19 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',
@@ -128,10 +128,12 @@ dependencies {
128128
}
129129
testImplementation "org.springframework.security:spring-security-test"
130130
testImplementation "org.testcontainers:elasticsearch:${versions.testcontainers}"
131+
testImplementation "org.testcontainers:localstack:${versions.testcontainers}"
132+
testImplementation "org.testcontainers:junit-jupiter:${versions.testcontainers}"
131133
testImplementation "org.junit.jupiter:junit-jupiter-api:${versions.junit}"
132134
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:${versions.junit}"
133135
testRuntimeOnly "org.testcontainers:postgresql:${versions.testcontainers}"
134-
136+
135137
gatling "io.gatling:gatling-core:${versions.gatling}"
136138
gatling "io.gatling:gatling-app:${versions.gatling}"
137139
}
@@ -198,8 +200,23 @@ task unitTests(type: Test) {
198200
exclude 'org/eclipse/openvsx/IntegrationTest.class'
199201
exclude 'org/eclipse/openvsx/cache/CacheServiceTest.class'
200202
exclude 'org/eclipse/openvsx/repositories/RepositoryServiceSmokeTest.class'
203+
exclude 'org/eclipse/openvsx/storage/AwsStorageServiceIntegrationTest.class'
201204
}
202205

206+
task s3IntegrationTests(type: Test) {
207+
description = 'Runs S3 integration tests using LocalStack (requires Docker/Podman).'
208+
group = 'verification'
209+
testClassesDirs = sourceSets.test.output.classesDirs
210+
classpath = sourceSets.test.runtimeClasspath
211+
useJUnitPlatform()
212+
include 'org/eclipse/openvsx/storage/AwsStorageServiceIntegrationTest.class'
213+
214+
// Set system properties for test configuration
215+
systemProperty 'spring.profiles.active', 's3-integration'
216+
}
217+
218+
219+
203220
jacocoTestReport {
204221
reports {
205222
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)