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)); + } }