From e4ce46685f7942fabe81a10b3574f6da7b2c05d2 Mon Sep 17 00:00:00 2001
From: Sergey Cheremisin <100359974+scheremisin@users.noreply.github.com>
Date: Fri, 8 May 2026 18:44:22 +0100
Subject: [PATCH] refresh S3 credentials from shared auth sources
---
pom.xml | 2 +-
.../s3/S3BlobStore.java | 53 +++----
.../s3/S3BlobStoreConfig.java | 150 ++++++++++++++++--
.../s3/S3BlobStoreConfigTest.java | 40 +++++
4 files changed, 200 insertions(+), 45 deletions(-)
diff --git a/pom.xml b/pom.xml
index 5bdf92fc..c3b13d2f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -14,7 +14,7 @@
hpi
- 999999-SNAPSHOT
+ 999999-joom-refresh-creds-SNAPSHOT
2.504
${jenkins.baseline}.3
diff --git a/src/main/java/io/jenkins/plugins/artifact_manager_jclouds/s3/S3BlobStore.java b/src/main/java/io/jenkins/plugins/artifact_manager_jclouds/s3/S3BlobStore.java
index 062bd056..ec839129 100644
--- a/src/main/java/io/jenkins/plugins/artifact_manager_jclouds/s3/S3BlobStore.java
+++ b/src/main/java/io/jenkins/plugins/artifact_manager_jclouds/s3/S3BlobStore.java
@@ -25,6 +25,7 @@
package io.jenkins.plugins.artifact_manager_jclouds.s3;
import java.io.IOException;
+import java.io.UncheckedIOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
@@ -54,14 +55,12 @@
import org.kohsuke.accmod.restrictions.NoExternalUse;
import org.kohsuke.stapler.DataBoundConstructor;
-import com.cloudbees.jenkins.plugins.awscredentials.AmazonWebServicesCredentials;
import com.google.common.base.Supplier;
import hudson.Extension;
import hudson.Util;
import io.jenkins.plugins.artifact_manager_jclouds.BlobStoreProvider;
import io.jenkins.plugins.artifact_manager_jclouds.BlobStoreProviderDescriptor;
-import io.jenkins.plugins.aws.global_configuration.CredentialsAwsGlobalConfiguration;
import org.jenkinsci.Symbol;
import software.amazon.awssdk.auth.credentials.AwsCredentials;
import software.amazon.awssdk.auth.credentials.AwsSessionCredentials;
@@ -161,42 +160,30 @@ public BlobStoreContext getContext() throws IOException {
* @throws IOException in case of error.
*/
private Supplier getCredentialsSupplier() throws IOException {
- // get user credentials from env vars, profiles,...
- String accessKeyId;
- String secretKey;
- String sessionToken;
- if (getConfiguration().getDisableSessionToken()) {
- AmazonWebServicesCredentials amazonWebServicesCredentials = CredentialsAwsGlobalConfiguration.get().getCredentials();
- if (amazonWebServicesCredentials == null) {
- throw new IOException("No static AWS credentials found");
+ resolveJcloudsCredentials();
+ return () -> {
+ try {
+ return resolveJcloudsCredentials();
+ } catch (IOException x) {
+ throw new UncheckedIOException("Failed to resolve AWS credentials", x);
}
- AwsCredentials awsCredentials = amazonWebServicesCredentials.resolveCredentials();
- accessKeyId = awsCredentials.accessKeyId();
- secretKey = awsCredentials.secretAccessKey();
- sessionToken = "";
- } else {
- AwsSessionCredentials awsSessionCredentials = CredentialsAwsGlobalConfiguration.get()
- .sessionCredentials(getRegion(), CredentialsAwsGlobalConfiguration.get().getCredentialsId());
- if(awsSessionCredentials != null ) {
- accessKeyId = awsSessionCredentials.accessKeyId();
- secretKey = awsSessionCredentials.secretAccessKey();
- sessionToken = awsSessionCredentials.sessionToken();
- } else {
- throw new IOException("No session AWS credentials found");
- }
- }
+ };
+ }
+ private SessionCredentials resolveJcloudsCredentials() throws IOException {
+ AwsCredentials awsCredentials = getConfiguration().resolveAwsCredentials(getConfiguration().getDisableSessionToken());
+ String sessionToken = "";
+ if (awsCredentials instanceof AwsSessionCredentials) {
+ sessionToken = ((AwsSessionCredentials) awsCredentials).sessionToken();
+ }
if (BREAK_CREDS) {
sessionToken = "";
}
-
- SessionCredentials sessionCredentials = SessionCredentials.builder()
- .accessKeyId(accessKeyId)
- .secretAccessKey(secretKey)
+ return SessionCredentials.builder()
+ .accessKeyId(awsCredentials.accessKeyId())
+ .secretAccessKey(awsCredentials.secretAccessKey())
.sessionToken(sessionToken)
.build();
-
- return () -> sessionCredentials;
}
@NonNull
@@ -212,11 +199,11 @@ public URI toURI(@NonNull String container, @NonNull String key) {
}
}
- public S3Presigner getS3Presigner(S3Client s3Client) {
+ public S3Presigner getS3Presigner(S3Client s3Client) throws IOException {
String customEndpoint = getConfiguration().getResolvedCustomEndpoint();
S3Presigner.Builder presignerBuilder = S3Presigner.builder()
.fipsEnabled(FIPS140.useCompliantAlgorithms())
- .credentialsProvider(CredentialsAwsGlobalConfiguration.get().getCredentials())
+ .credentialsProvider(getConfiguration().getAwsCredentialsProvider(getConfiguration().getDisableSessionToken()))
.s3Client(s3Client);
if (StringUtils.isNotBlank(customEndpoint)) {
presignerBuilder.endpointOverride(URI.create(customEndpoint));
diff --git a/src/main/java/io/jenkins/plugins/artifact_manager_jclouds/s3/S3BlobStoreConfig.java b/src/main/java/io/jenkins/plugins/artifact_manager_jclouds/s3/S3BlobStoreConfig.java
index 9905b794..83e047c6 100644
--- a/src/main/java/io/jenkins/plugins/artifact_manager_jclouds/s3/S3BlobStoreConfig.java
+++ b/src/main/java/io/jenkins/plugins/artifact_manager_jclouds/s3/S3BlobStoreConfig.java
@@ -25,11 +25,18 @@
package io.jenkins.plugins.artifact_manager_jclouds.s3;
import java.io.IOException;
+import java.io.UncheckedIOException;
import java.net.URI;
import java.net.URISyntaxException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.LinkedHashMap;
+import java.util.Map;
import java.util.logging.Logger;
import java.util.regex.Pattern;
+import com.cloudbees.jenkins.plugins.awscredentials.AmazonWebServicesCredentials;
import jenkins.util.SystemProperties;
import org.apache.commons.lang.StringUtils;
import org.kohsuke.stapler.DataBoundSetter;
@@ -55,8 +62,11 @@
import jenkins.model.Jenkins;
import jenkins.security.FIPS140;
import org.jenkinsci.Symbol;
+import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
+import software.amazon.awssdk.auth.credentials.AwsCredentials;
+import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
import software.amazon.awssdk.auth.credentials.AwsSessionCredentials;
-import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
+import software.amazon.awssdk.auth.credentials.WebIdentityTokenFileCredentialsProvider;
import software.amazon.awssdk.core.exception.SdkClientException;
import software.amazon.awssdk.core.SdkSystemSetting;
import software.amazon.awssdk.regions.Region;
@@ -347,19 +357,137 @@ public Region getRegion() {
return Region.of(regionStr);
}
- private S3ClientBuilder getAmazonS3ClientBuilderWithCredentials(boolean disableSessionToken) throws IOException, URISyntaxException {
- S3ClientBuilder builder = getAmazonS3ClientBuilder();
+ AwsCredentialsProvider getAwsCredentialsProvider(boolean disableSessionToken) throws IOException {
+ resolveAwsCredentials(disableSessionToken);
+ return () -> {
+ try {
+ return resolveAwsCredentials(disableSessionToken);
+ } catch (IOException x) {
+ throw new UncheckedIOException("Failed to resolve AWS credentials", x);
+ }
+ };
+ }
+
+ @VisibleForTesting
+ AwsCredentials resolveAwsCredentials(boolean disableSessionToken) throws IOException {
if (disableSessionToken) {
- builder = builder.credentialsProvider(CredentialsAwsGlobalConfiguration.get().getCredentials());
- } else {
- AwsSessionCredentials awsSessionCredentials = CredentialsAwsGlobalConfiguration.get()
- .sessionCredentials(getRegion().id(), CredentialsAwsGlobalConfiguration.get().getCredentialsId());
- if(awsSessionCredentials != null ) {
- builder.credentialsProvider(StaticCredentialsProvider.create(awsSessionCredentials));
- } else {
- throw new IOException("No session AWS credentials found");
+ return resolveStaticAwsCredentials();
+ }
+ return resolveSessionCredentials(getRegion().id());
+ }
+
+ private AwsCredentials resolveStaticAwsCredentials() throws IOException {
+ AwsCredentials sharedCredentials = resolveSharedFileCredentials(false);
+ if (sharedCredentials != null) {
+ return AwsBasicCredentials.create(sharedCredentials.accessKeyId(), sharedCredentials.secretAccessKey());
+ }
+ AmazonWebServicesCredentials amazonWebServicesCredentials = CredentialsAwsGlobalConfiguration.get().getCredentials();
+ if (amazonWebServicesCredentials == null) {
+ throw new IOException("No static AWS credentials found");
+ }
+ AwsCredentials awsCredentials = amazonWebServicesCredentials.resolveCredentials();
+ return AwsBasicCredentials.create(awsCredentials.accessKeyId(), awsCredentials.secretAccessKey());
+ }
+
+ @VisibleForTesting
+ AwsSessionCredentials resolveSessionCredentials(String region) throws IOException {
+ if (Util.fixEmpty(CredentialsAwsGlobalConfiguration.get().getCredentialsId()) == null) {
+ AwsSessionCredentials sharedCredentials = resolveSharedFileSessionCredentials();
+ if (sharedCredentials != null) {
+ return sharedCredentials;
+ }
+ AwsSessionCredentials webIdentityCredentials = resolveWebIdentitySessionCredentials();
+ if (webIdentityCredentials != null) {
+ return webIdentityCredentials;
}
}
+ AwsSessionCredentials awsSessionCredentials = CredentialsAwsGlobalConfiguration.get()
+ .sessionCredentials(region, CredentialsAwsGlobalConfiguration.get().getCredentialsId());
+ if (awsSessionCredentials == null) {
+ throw new IOException("No session AWS credentials found");
+ }
+ return awsSessionCredentials;
+ }
+
+ private AwsSessionCredentials resolveSharedFileSessionCredentials() throws IOException {
+ AwsCredentials awsCredentials = resolveSharedFileCredentials(true);
+ return awsCredentials instanceof AwsSessionCredentials ? (AwsSessionCredentials) awsCredentials : null;
+ }
+
+ private AwsSessionCredentials resolveWebIdentitySessionCredentials() {
+ if (Util.fixEmptyAndTrim(System.getenv("AWS_WEB_IDENTITY_TOKEN_FILE")) == null
+ || Util.fixEmptyAndTrim(System.getenv("AWS_ROLE_ARN")) == null) {
+ return null;
+ }
+ AwsCredentials awsCredentials = WebIdentityTokenFileCredentialsProvider.create().resolveCredentials();
+ return awsCredentials instanceof AwsSessionCredentials ? (AwsSessionCredentials) awsCredentials : null;
+ }
+
+ private AwsCredentials resolveSharedFileCredentials(boolean requireSessionToken) throws IOException {
+ String sharedCredentialsFile = Util.fixEmptyAndTrim(System.getenv("AWS_SHARED_CREDENTIALS_FILE"));
+ if (sharedCredentialsFile == null) {
+ return null;
+ }
+ return loadSharedFileCredentials(Paths.get(sharedCredentialsFile), getAwsProfileName(), requireSessionToken);
+ }
+
+ @VisibleForTesting
+ static AwsCredentials loadSharedFileCredentials(Path path, String profileName, boolean requireSessionToken) throws IOException {
+ Map profileEntries = loadSharedFileProfile(path, profileName);
+ String accessKeyId = Util.fixEmptyAndTrim(profileEntries.get("aws_access_key_id"));
+ String secretAccessKey = Util.fixEmptyAndTrim(profileEntries.get("aws_secret_access_key"));
+ String sessionToken = Util.fixEmptyAndTrim(profileEntries.get("aws_session_token"));
+ if (sessionToken == null) {
+ sessionToken = Util.fixEmptyAndTrim(profileEntries.get("aws_security_token"));
+ }
+ if (accessKeyId == null || secretAccessKey == null) {
+ return null;
+ }
+ if (requireSessionToken) {
+ return sessionToken == null ? null : AwsSessionCredentials.create(accessKeyId, secretAccessKey, sessionToken);
+ }
+ return AwsBasicCredentials.create(accessKeyId, secretAccessKey);
+ }
+
+ @VisibleForTesting
+ static Map loadSharedFileProfile(Path path, String profileName) throws IOException {
+ Map profileEntries = new LinkedHashMap<>();
+ if (!Files.isRegularFile(path)) {
+ return profileEntries;
+ }
+ String currentProfile = null;
+ for (String rawLine : Files.readAllLines(path)) {
+ String line = rawLine.trim();
+ if (line.isEmpty() || line.startsWith("#") || line.startsWith(";")) {
+ continue;
+ }
+ if (line.startsWith("[") && line.endsWith("]")) {
+ currentProfile = line.substring(1, line.length() - 1).trim();
+ continue;
+ }
+ if (!profileName.equals(currentProfile)) {
+ continue;
+ }
+ int separator = line.indexOf('=');
+ if (separator < 0) {
+ continue;
+ }
+ profileEntries.put(line.substring(0, separator).trim(), line.substring(separator + 1).trim());
+ }
+ return profileEntries;
+ }
+
+ private String getAwsProfileName() {
+ String profileName = Util.fixEmptyAndTrim(System.getenv("AWS_PROFILE"));
+ if (profileName == null) {
+ profileName = Util.fixEmptyAndTrim(System.getenv("AWS_DEFAULT_PROFILE"));
+ }
+ return profileName != null ? profileName : "default";
+ }
+
+ private S3ClientBuilder getAmazonS3ClientBuilderWithCredentials(boolean disableSessionToken) throws IOException, URISyntaxException {
+ S3ClientBuilder builder = getAmazonS3ClientBuilder();
+ builder = builder.credentialsProvider(getAwsCredentialsProvider(disableSessionToken));
return builder;
}
diff --git a/src/test/java/io/jenkins/plugins/artifact_manager_jclouds/s3/S3BlobStoreConfigTest.java b/src/test/java/io/jenkins/plugins/artifact_manager_jclouds/s3/S3BlobStoreConfigTest.java
index daf8820e..81b81a7a 100644
--- a/src/test/java/io/jenkins/plugins/artifact_manager_jclouds/s3/S3BlobStoreConfigTest.java
+++ b/src/test/java/io/jenkins/plugins/artifact_manager_jclouds/s3/S3BlobStoreConfigTest.java
@@ -1,10 +1,13 @@
package io.jenkins.plugins.artifact_manager_jclouds.s3;
+import java.nio.file.Files;
+import java.nio.file.Path;
import java.util.logging.Logger;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
import org.jvnet.hudson.test.JenkinsRule;
import io.jenkins.plugins.artifact_manager_jclouds.BlobStoreProvider;
import io.jenkins.plugins.artifact_manager_jclouds.JCloudsArtifactManagerFactory;
@@ -13,11 +16,15 @@
import hudson.util.FormValidation;
import jenkins.model.ArtifactManagerConfiguration;
+import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
+import software.amazon.awssdk.auth.credentials.AwsCredentials;
+import software.amazon.awssdk.auth.credentials.AwsSessionCredentials;
import software.amazon.awssdk.regions.Region;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.instanceOf;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
@@ -37,6 +44,9 @@ public class S3BlobStoreConfigTest {
@Rule
public JenkinsRule j = new JenkinsRule();
+ @Rule
+ public TemporaryFolder tmp = new TemporaryFolder();
+
@Test
public void checkConfigurationManually() throws Exception {
S3BlobStore provider = new S3BlobStore();
@@ -170,4 +180,34 @@ public void checkValidationUseHttpsWithFipsDisabled() {
assertEquals(descriptor.doCheckUseHttp(true).kind , FormValidation.Kind.OK);
assertEquals(descriptor.doCheckUseHttp(false).kind , FormValidation.Kind.OK);
}
+
+ @Test
+ public void loadSharedFileCredentialsReadsLatestSessionToken() throws Exception {
+ Path credentialsFile = tmp.newFile("aws-credentials").toPath();
+
+ Files.writeString(credentialsFile, "[default]\naws_access_key_id=first-key\naws_secret_access_key=first-secret\naws_session_token=first-token\n");
+ AwsCredentials initial = S3BlobStoreConfig.loadSharedFileCredentials(credentialsFile, "default", true);
+ assertEquals("first-key", initial.accessKeyId());
+ assertEquals("first-secret", initial.secretAccessKey());
+ assertEquals("first-token", ((AwsSessionCredentials) initial).sessionToken());
+
+ Files.writeString(credentialsFile, "[default]\naws_access_key_id=second-key\naws_secret_access_key=second-secret\naws_session_token=second-token\n");
+ AwsCredentials rotated = S3BlobStoreConfig.loadSharedFileCredentials(credentialsFile, "default", true);
+ assertEquals("second-key", rotated.accessKeyId());
+ assertEquals("second-secret", rotated.secretAccessKey());
+ assertEquals("second-token", ((AwsSessionCredentials) rotated).sessionToken());
+ }
+
+ @Test
+ public void loadSharedFileCredentialsReturnsBasicCredentialsWithoutToken() throws Exception {
+ Path credentialsFile = tmp.newFile("aws-basic-credentials").toPath();
+
+ Files.writeString(credentialsFile, "[default]\naws_access_key_id=static-key\naws_secret_access_key=static-secret\n");
+
+ AwsCredentials staticCredentials = S3BlobStoreConfig.loadSharedFileCredentials(credentialsFile, "default", false);
+ assertThat(staticCredentials, instanceOf(AwsBasicCredentials.class));
+ assertEquals("static-key", staticCredentials.accessKeyId());
+ assertEquals("static-secret", staticCredentials.secretAccessKey());
+ assertNull(S3BlobStoreConfig.loadSharedFileCredentials(credentialsFile, "default", true));
+ }
}