From dd6ace5edeb96323681406b2139102545a9e15f6 Mon Sep 17 00:00:00 2001 From: Andriy Redko Date: Thu, 2 Jun 2022 12:56:18 -0400 Subject: [PATCH] Support use of IRSA for repository-s3 plugin credentials (#3475) * Support use of IRSA for repository-s3 plugin credentials Signed-off-by: Andriy Redko * Address code review comments Signed-off-by: Andriy Redko * Address code review comments Signed-off-by: Andriy Redko --- plugins/repository-s3/build.gradle | 1 + .../aws-java-sdk-sts-1.11.749.jar.sha1 | 1 + .../repositories/s3/AmazonS3Reference.java | 26 +++- .../s3/AmazonS3WithCredentials.java | 39 ++++++ .../repositories/s3/S3ClientSettings.java | 94 ++++++++++++- .../repositories/s3/S3RepositoryPlugin.java | 5 +- .../opensearch/repositories/s3/S3Service.java | 125 ++++++++++++++++-- .../s3/AwsS3ServiceImplTests.java | 105 +++++++++++++++ .../s3/RepositoryCredentialsTests.java | 7 +- .../s3/S3ClientSettingsTests.java | 49 ++++++- .../repositories/s3/S3ServiceTests.java | 29 +++- 11 files changed, 464 insertions(+), 17 deletions(-) create mode 100644 plugins/repository-s3/licenses/aws-java-sdk-sts-1.11.749.jar.sha1 create mode 100644 plugins/repository-s3/src/main/java/org/opensearch/repositories/s3/AmazonS3WithCredentials.java diff --git a/plugins/repository-s3/build.gradle b/plugins/repository-s3/build.gradle index 33448b0039ce2..54a2593f4c6f4 100644 --- a/plugins/repository-s3/build.gradle +++ b/plugins/repository-s3/build.gradle @@ -51,6 +51,7 @@ versions << [ dependencies { api "com.amazonaws:aws-java-sdk-s3:${versions.aws}" api "com.amazonaws:aws-java-sdk-core:${versions.aws}" + api "com.amazonaws:aws-java-sdk-sts:${versions.aws}" api "com.amazonaws:jmespath-java:${versions.aws}" api "org.apache.httpcomponents:httpclient:${versions.httpclient}" api "org.apache.httpcomponents:httpcore:${versions.httpcore}" diff --git a/plugins/repository-s3/licenses/aws-java-sdk-sts-1.11.749.jar.sha1 b/plugins/repository-s3/licenses/aws-java-sdk-sts-1.11.749.jar.sha1 new file mode 100644 index 0000000000000..29c9a93542058 --- /dev/null +++ b/plugins/repository-s3/licenses/aws-java-sdk-sts-1.11.749.jar.sha1 @@ -0,0 +1 @@ +724bd22c0ff41c496469e18f9bea12bdfb2f7540 \ No newline at end of file diff --git a/plugins/repository-s3/src/main/java/org/opensearch/repositories/s3/AmazonS3Reference.java b/plugins/repository-s3/src/main/java/org/opensearch/repositories/s3/AmazonS3Reference.java index 62e415705a011..6f14cd850ccf6 100644 --- a/plugins/repository-s3/src/main/java/org/opensearch/repositories/s3/AmazonS3Reference.java +++ b/plugins/repository-s3/src/main/java/org/opensearch/repositories/s3/AmazonS3Reference.java @@ -32,17 +32,39 @@ package org.opensearch.repositories.s3; +import com.amazonaws.auth.AWSCredentialsProvider; import com.amazonaws.services.s3.AmazonS3; import com.amazonaws.services.s3.AmazonS3Client; + +import org.opensearch.common.Nullable; import org.opensearch.common.concurrent.RefCountedReleasable; +import java.io.Closeable; +import java.io.IOException; + /** * Handles the shutdown of the wrapped {@link AmazonS3Client} using reference * counting. */ public class AmazonS3Reference extends RefCountedReleasable { - AmazonS3Reference(AmazonS3 client) { - super("AWS_S3_CLIENT", client, client::shutdown); + this(client, null); + } + + AmazonS3Reference(AmazonS3WithCredentials client) { + this(client.client(), client.credentials()); + } + + AmazonS3Reference(AmazonS3 client, @Nullable AWSCredentialsProvider credentials) { + super("AWS_S3_CLIENT", client, () -> { + client.shutdown(); + if (credentials instanceof Closeable) { + try { + ((Closeable) credentials).close(); + } catch (IOException e) { + /* Do nothing here */ + } + } + }); } } diff --git a/plugins/repository-s3/src/main/java/org/opensearch/repositories/s3/AmazonS3WithCredentials.java b/plugins/repository-s3/src/main/java/org/opensearch/repositories/s3/AmazonS3WithCredentials.java new file mode 100644 index 0000000000000..5622be5546cb1 --- /dev/null +++ b/plugins/repository-s3/src/main/java/org/opensearch/repositories/s3/AmazonS3WithCredentials.java @@ -0,0 +1,39 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.repositories.s3; + +import com.amazonaws.auth.AWSCredentialsProvider; +import com.amazonaws.services.s3.AmazonS3; + +import org.opensearch.common.Nullable; + +/** + * The holder of the AmazonS3 and AWSCredentialsProvider + */ +final class AmazonS3WithCredentials { + private final AmazonS3 client; + private final AWSCredentialsProvider credentials; + + private AmazonS3WithCredentials(final AmazonS3 client, @Nullable final AWSCredentialsProvider credentials) { + this.client = client; + this.credentials = credentials; + } + + AmazonS3 client() { + return client; + } + + AWSCredentialsProvider credentials() { + return credentials; + } + + static AmazonS3WithCredentials create(final AmazonS3 client, @Nullable final AWSCredentialsProvider credentials) { + return new AmazonS3WithCredentials(client, credentials); + } +} diff --git a/plugins/repository-s3/src/main/java/org/opensearch/repositories/s3/S3ClientSettings.java b/plugins/repository-s3/src/main/java/org/opensearch/repositories/s3/S3ClientSettings.java index e02c7cae89378..1f9af5314f30d 100644 --- a/plugins/repository-s3/src/main/java/org/opensearch/repositories/s3/S3ClientSettings.java +++ b/plugins/repository-s3/src/main/java/org/opensearch/repositories/s3/S3ClientSettings.java @@ -67,6 +67,29 @@ final class S3ClientSettings { /** Placeholder client name for normalizing client settings in the repository settings. */ private static final String PLACEHOLDER_CLIENT = "placeholder"; + // Properties to support using IAM Roles for Service Accounts (IRSA) + + /** The identity token file for connecting to s3. */ + static final Setting.AffixSetting IDENTITY_TOKEN_FILE_SETTING = Setting.affixKeySetting( + PREFIX, + "identity_token_file", + key -> SecureSetting.simpleString(key, Property.NodeScope) + ); + + /** The role ARN (Amazon Resource Name) for connecting to s3. */ + static final Setting.AffixSetting ROLE_ARN_SETTING = Setting.affixKeySetting( + PREFIX, + "role_arn", + key -> SecureSetting.secureString(key, null) + ); + + /** The role session name for connecting to s3. */ + static final Setting.AffixSetting ROLE_SESSION_NAME_SETTING = Setting.affixKeySetting( + PREFIX, + "role_session_name", + key -> SecureSetting.secureString(key, null) + ); + /** The access key (ie login id) for connecting to s3. */ static final Setting.AffixSetting ACCESS_KEY_SETTING = Setting.affixKeySetting( PREFIX, @@ -189,6 +212,9 @@ final class S3ClientSettings { /** Credentials to authenticate with s3. */ final S3BasicCredentials credentials; + /** Credentials to authenticate with s3 using IAM Roles for Service Accounts (IRSA). */ + final IrsaCredentials irsaCredentials; + /** The s3 endpoint the client should talk to, or empty string to use the default. */ final String endpoint; @@ -221,6 +247,7 @@ final class S3ClientSettings { private S3ClientSettings( S3BasicCredentials credentials, + IrsaCredentials irsaCredentials, String endpoint, Protocol protocol, int readTimeoutMillis, @@ -233,6 +260,7 @@ private S3ClientSettings( ProxySettings proxySettings ) { this.credentials = credentials; + this.irsaCredentials = irsaCredentials; this.endpoint = endpoint; this.protocol = protocol; this.readTimeoutMillis = readTimeoutMillis; @@ -301,6 +329,7 @@ S3ClientSettings refine(Settings repositorySettings) { validateInetAddressFor(newProxyHost); return new S3ClientSettings( newCredentials, + irsaCredentials, newEndpoint, newProtocol, newReadTimeoutMillis, @@ -396,12 +425,27 @@ private static S3BasicCredentials loadCredentials(Settings settings, String clie } } + private static IrsaCredentials loadIrsaCredentials(Settings settings, String clientName) { + String identityTokenFile = getConfigValue(settings, clientName, IDENTITY_TOKEN_FILE_SETTING); + try ( + SecureString roleArn = getConfigValue(settings, clientName, ROLE_ARN_SETTING); + SecureString roleSessionName = getConfigValue(settings, clientName, ROLE_SESSION_NAME_SETTING) + ) { + if (identityTokenFile.length() != 0 || roleArn.length() != 0 || roleSessionName.length() != 0) { + return new IrsaCredentials(identityTokenFile.toString(), roleArn.toString(), roleSessionName.toString()); + } + + return null; + } + } + // pkg private for tests /** Parse settings for a single client. */ static S3ClientSettings getClientSettings(final Settings settings, final String clientName) { final Protocol awsProtocol = getConfigValue(settings, clientName, PROTOCOL_SETTING); return new S3ClientSettings( S3ClientSettings.loadCredentials(settings, clientName), + S3ClientSettings.loadIrsaCredentials(settings, clientName), getConfigValue(settings, clientName, ENDPOINT_SETTING), awsProtocol, Math.toIntExact(getConfigValue(settings, clientName, READ_TIMEOUT_SETTING).millis()), @@ -482,7 +526,8 @@ public boolean equals(final Object o) { && proxySettings.equals(that.proxySettings) && Objects.equals(disableChunkedEncoding, that.disableChunkedEncoding) && Objects.equals(region, that.region) - && Objects.equals(signerOverride, that.signerOverride); + && Objects.equals(signerOverride, that.signerOverride) + && Objects.equals(irsaCredentials, that.irsaCredentials); } @Override @@ -512,4 +557,51 @@ private static T getRepoSettingOrDefault(Setting.AffixSetting setting, Se } return defaultValue; } + + /** + * Class to store IAM Roles for Service Accounts (IRSA) credentials + * See please: https://docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts.html + */ + static class IrsaCredentials { + private final String identityTokenFile; + private final String roleArn; + private final String roleSessionName; + + IrsaCredentials(String identityTokenFile, String roleArn, String roleSessionName) { + this.identityTokenFile = Strings.isNullOrEmpty(identityTokenFile) ? null : identityTokenFile; + this.roleArn = Strings.isNullOrEmpty(roleArn) ? null : roleArn; + this.roleSessionName = Strings.isNullOrEmpty(roleSessionName) ? "s3-sdk-java-" + System.currentTimeMillis() : roleSessionName; + } + + public String getIdentityTokenFile() { + return identityTokenFile; + } + + public String getRoleArn() { + return roleArn; + } + + public String getRoleSessionName() { + return roleSessionName; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final IrsaCredentials that = (IrsaCredentials) o; + return Objects.equals(identityTokenFile, that.identityTokenFile) + && Objects.equals(roleArn, that.roleArn) + && Objects.equals(roleSessionName, that.roleSessionName); + } + + @Override + public int hashCode() { + return Objects.hash(identityTokenFile, roleArn, roleSessionName); + } + } } diff --git a/plugins/repository-s3/src/main/java/org/opensearch/repositories/s3/S3RepositoryPlugin.java b/plugins/repository-s3/src/main/java/org/opensearch/repositories/s3/S3RepositoryPlugin.java index 679243b28cfc7..e1ea31dc53d1e 100644 --- a/plugins/repository-s3/src/main/java/org/opensearch/repositories/s3/S3RepositoryPlugin.java +++ b/plugins/repository-s3/src/main/java/org/opensearch/repositories/s3/S3RepositoryPlugin.java @@ -132,7 +132,10 @@ public List> getSettings() { S3Repository.ACCESS_KEY_SETTING, S3Repository.SECRET_KEY_SETTING, S3ClientSettings.SIGNER_OVERRIDE, - S3ClientSettings.REGION + S3ClientSettings.REGION, + S3ClientSettings.ROLE_ARN_SETTING, + S3ClientSettings.IDENTITY_TOKEN_FILE_SETTING, + S3ClientSettings.ROLE_SESSION_NAME_SETTING ); } diff --git a/plugins/repository-s3/src/main/java/org/opensearch/repositories/s3/S3Service.java b/plugins/repository-s3/src/main/java/org/opensearch/repositories/s3/S3Service.java index 3ce19378ac05c..6919549874445 100644 --- a/plugins/repository-s3/src/main/java/org/opensearch/repositories/s3/S3Service.java +++ b/plugins/repository-s3/src/main/java/org/opensearch/repositories/s3/S3Service.java @@ -35,8 +35,11 @@ import com.amazonaws.ClientConfiguration; import com.amazonaws.auth.AWSCredentials; import com.amazonaws.auth.AWSCredentialsProvider; +import com.amazonaws.auth.AWSSessionCredentialsProvider; import com.amazonaws.auth.AWSStaticCredentialsProvider; import com.amazonaws.auth.EC2ContainerCredentialsProviderWrapper; +import com.amazonaws.auth.STSAssumeRoleSessionCredentialsProvider; +import com.amazonaws.auth.STSAssumeRoleWithWebIdentitySessionCredentialsProvider; import com.amazonaws.client.builder.AwsClientBuilder; import com.amazonaws.http.IdleConnectionReaper; import com.amazonaws.http.SystemPropertyTlsKeyManagersProvider; @@ -45,6 +48,8 @@ import com.amazonaws.services.s3.AmazonS3; import com.amazonaws.services.s3.AmazonS3ClientBuilder; import com.amazonaws.services.s3.internal.Constants; +import com.amazonaws.services.securitytoken.AWSSecurityTokenService; +import com.amazonaws.services.securitytoken.AWSSecurityTokenServiceClientBuilder; import org.apache.http.conn.ssl.DefaultHostnameVerifier; import org.apache.http.conn.ssl.SSLConnectionSocketFactory; @@ -52,9 +57,11 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.opensearch.cluster.metadata.RepositoryMetadata; +import org.opensearch.common.Nullable; import org.opensearch.common.Strings; import org.opensearch.common.collect.MapBuilder; import org.opensearch.common.settings.Settings; +import org.opensearch.repositories.s3.S3ClientSettings.IrsaCredentials; import javax.net.ssl.SSLContext; import java.io.Closeable; @@ -67,6 +74,9 @@ import java.security.SecureRandom; import java.util.Map; +import static com.amazonaws.SDKGlobalConfiguration.AWS_ROLE_ARN_ENV_VAR; +import static com.amazonaws.SDKGlobalConfiguration.AWS_ROLE_SESSION_NAME_ENV_VAR; +import static com.amazonaws.SDKGlobalConfiguration.AWS_WEB_IDENTITY_ENV_VAR; import static java.util.Collections.emptyMap; class S3Service implements Closeable { @@ -163,9 +173,11 @@ S3ClientSettings settings(RepositoryMetadata repositoryMetadata) { } // proxy for testing - AmazonS3 buildClient(final S3ClientSettings clientSettings) { + AmazonS3WithCredentials buildClient(final S3ClientSettings clientSettings) { final AmazonS3ClientBuilder builder = AmazonS3ClientBuilder.standard(); - builder.withCredentials(buildCredentials(logger, clientSettings)); + + final AWSCredentialsProvider credentials = buildCredentials(logger, clientSettings); + builder.withCredentials(credentials); builder.withClientConfiguration(buildConfiguration(clientSettings)); String endpoint = Strings.hasLength(clientSettings.endpoint) ? clientSettings.endpoint : Constants.S3_HOSTNAME; @@ -192,7 +204,8 @@ AmazonS3 buildClient(final S3ClientSettings clientSettings) { if (clientSettings.disableChunkedEncoding) { builder.disableChunkedEncoding(); } - return SocketAccess.doPrivileged(builder::build); + final AmazonS3 client = SocketAccess.doPrivileged(builder::build); + return AmazonS3WithCredentials.create(client, credentials); } // pkg private for tests @@ -258,24 +271,83 @@ public Socket createSocket(final HttpContext ctx) throws IOException { // pkg private for tests static AWSCredentialsProvider buildCredentials(Logger logger, S3ClientSettings clientSettings) { - final S3BasicCredentials credentials = clientSettings.credentials; - if (credentials == null) { + final S3BasicCredentials basicCredentials = clientSettings.credentials; + final IrsaCredentials irsaCredentials = buildFromEnviroment(clientSettings.irsaCredentials); + + // If IAM Roles for Service Accounts (IRSA) credentials are configured, start with them first + if (irsaCredentials != null) { + logger.debug("Using IRSA credentials"); + + AWSSecurityTokenService securityTokenService = null; + final String region = Strings.hasLength(clientSettings.region) ? clientSettings.region : null; + if (region != null || basicCredentials != null) { + securityTokenService = SocketAccess.doPrivileged( + () -> AWSSecurityTokenServiceClientBuilder.standard() + .withCredentials((basicCredentials != null) ? new AWSStaticCredentialsProvider(basicCredentials) : null) + .withRegion(region) + .build() + ); + } + + if (irsaCredentials.getIdentityTokenFile() == null) { + return new PrivilegedSTSAssumeRoleSessionCredentialsProvider<>( + securityTokenService, + new STSAssumeRoleSessionCredentialsProvider.Builder(irsaCredentials.getRoleArn(), irsaCredentials.getRoleSessionName()) + .withStsClient(securityTokenService) + .build() + ); + } else { + return new PrivilegedSTSAssumeRoleSessionCredentialsProvider<>( + securityTokenService, + new STSAssumeRoleWithWebIdentitySessionCredentialsProvider.Builder( + irsaCredentials.getRoleArn(), + irsaCredentials.getRoleSessionName(), + irsaCredentials.getIdentityTokenFile() + ).withStsClient(securityTokenService).build() + ); + } + } else if (basicCredentials != null) { + logger.debug("Using basic key/secret credentials"); + return new AWSStaticCredentialsProvider(basicCredentials); + } else { logger.debug("Using instance profile credentials"); return new PrivilegedInstanceProfileCredentialsProvider(); - } else { - logger.debug("Using basic key/secret credentials"); - return new AWSStaticCredentialsProvider(credentials); } } + private static IrsaCredentials buildFromEnviroment(IrsaCredentials defaults) { + if (defaults == null) { + return null; + } + + String webIdentityTokenFile = defaults.getIdentityTokenFile(); + if (webIdentityTokenFile == null) { + webIdentityTokenFile = System.getenv(AWS_WEB_IDENTITY_ENV_VAR); + } + + String roleArn = defaults.getRoleArn(); + if (roleArn == null) { + roleArn = System.getenv(AWS_ROLE_ARN_ENV_VAR); + } + + String roleSessionName = defaults.getRoleSessionName(); + if (roleSessionName == null) { + roleSessionName = System.getenv(AWS_ROLE_SESSION_NAME_ENV_VAR); + } + + return new IrsaCredentials(webIdentityTokenFile, roleArn, roleSessionName); + } + private synchronized void releaseCachedClients() { // the clients will shutdown when they will not be used anymore for (final AmazonS3Reference clientReference : clientsCache.values()) { clientReference.decRef(); } + // clear previously cached clients, they will be build lazily clientsCache = emptyMap(); derivedClientSettings = emptyMap(); + // shutdown IdleConnectionReaper background thread // it will be restarted on new client usage IdleConnectionReaper.shutdown(); @@ -300,6 +372,43 @@ public void refresh() { } } + static class PrivilegedSTSAssumeRoleSessionCredentialsProvider

+ implements + AWSCredentialsProvider, + Closeable { + private final P credentials; + private final AWSSecurityTokenService securityTokenService; + + private PrivilegedSTSAssumeRoleSessionCredentialsProvider( + @Nullable final AWSSecurityTokenService securityTokenService, + final P credentials + ) { + this.securityTokenService = securityTokenService; + this.credentials = credentials; + } + + @Override + public AWSCredentials getCredentials() { + return SocketAccess.doPrivileged(credentials::getCredentials); + } + + @Override + public void refresh() { + SocketAccess.doPrivilegedVoid(credentials::refresh); + } + + @Override + public void close() throws IOException { + SocketAccess.doPrivilegedIOException(() -> { + credentials.close(); + if (securityTokenService != null) { + securityTokenService.shutdown(); + } + return null; + }); + }; + } + @Override public void close() { releaseCachedClients(); diff --git a/plugins/repository-s3/src/test/java/org/opensearch/repositories/s3/AwsS3ServiceImplTests.java b/plugins/repository-s3/src/test/java/org/opensearch/repositories/s3/AwsS3ServiceImplTests.java index 38d9ebf337731..76bd5d303e5fb 100644 --- a/plugins/repository-s3/src/test/java/org/opensearch/repositories/s3/AwsS3ServiceImplTests.java +++ b/plugins/repository-s3/src/test/java/org/opensearch/repositories/s3/AwsS3ServiceImplTests.java @@ -36,11 +36,16 @@ import com.amazonaws.Protocol; import com.amazonaws.auth.AWSCredentialsProvider; import com.amazonaws.auth.AWSStaticCredentialsProvider; +import com.amazonaws.http.IdleConnectionReaper; + +import org.junit.AfterClass; import org.opensearch.common.settings.MockSecureSettings; import org.opensearch.common.settings.Settings; import org.opensearch.test.OpenSearchTestCase; +import java.io.Closeable; import java.io.IOException; +import java.util.HashMap; import java.util.Locale; import java.util.Map; @@ -51,6 +56,11 @@ import static org.opensearch.repositories.s3.S3ClientSettings.PROXY_TYPE_SETTING; public class AwsS3ServiceImplTests extends OpenSearchTestCase { + @AfterClass + public static void shutdownIdleConnectionReaper() { + // created by default STS client + IdleConnectionReaper.shutdown(); + } public void testAWSCredentialsDefaultToInstanceProviders() { final String inexistentClientName = randomAlphaOfLength(8).toLowerCase(Locale.ROOT); @@ -86,6 +96,101 @@ public void testAWSCredentialsFromKeystore() { assertThat(defaultCredentialsProvider, instanceOf(S3Service.PrivilegedInstanceProfileCredentialsProvider.class)); } + public void testCredentialsAndIrsaWithIdentityTokenFileCredentialsFromKeystore() throws IOException { + final Map plainSettings = new HashMap<>(); + final MockSecureSettings secureSettings = new MockSecureSettings(); + final String clientNamePrefix = "some_client_name_"; + final int clientsCount = randomIntBetween(0, 4); + for (int i = 0; i < clientsCount; i++) { + final String clientName = clientNamePrefix + i; + secureSettings.setString("s3.client." + clientName + ".role_arn", clientName + "_role_arn"); + + // Use static AWS credentials for tests + secureSettings.setString("s3.client." + clientName + ".access_key", clientName + "_aws_access_key"); + secureSettings.setString("s3.client." + clientName + ".secret_key", clientName + "_aws_secret_key"); + + // Use explicit region setting + plainSettings.put("s3.client." + clientName + ".region", "us-east1"); + plainSettings.put("s3.client." + clientName + ".identity_token_file", clientName + "_identity_token_file"); + } + final Settings settings = Settings.builder().loadFromMap(plainSettings).setSecureSettings(secureSettings).build(); + final Map allClientsSettings = S3ClientSettings.load(settings); + // no less, no more + assertThat(allClientsSettings.size(), is(clientsCount + 1)); // including default + for (int i = 0; i < clientsCount; i++) { + final String clientName = clientNamePrefix + i; + final S3ClientSettings someClientSettings = allClientsSettings.get(clientName); + final AWSCredentialsProvider credentialsProvider = S3Service.buildCredentials(logger, someClientSettings); + assertThat(credentialsProvider, instanceOf(S3Service.PrivilegedSTSAssumeRoleSessionCredentialsProvider.class)); + ((Closeable) credentialsProvider).close(); + } + // test default exists and is an Instance provider + final S3ClientSettings defaultClientSettings = allClientsSettings.get("default"); + final AWSCredentialsProvider defaultCredentialsProvider = S3Service.buildCredentials(logger, defaultClientSettings); + assertThat(defaultCredentialsProvider, instanceOf(S3Service.PrivilegedInstanceProfileCredentialsProvider.class)); + } + + public void testCredentialsAndIrsaCredentialsFromKeystore() throws IOException { + final Map plainSettings = new HashMap<>(); + final MockSecureSettings secureSettings = new MockSecureSettings(); + final String clientNamePrefix = "some_client_name_"; + final int clientsCount = randomIntBetween(0, 4); + for (int i = 0; i < clientsCount; i++) { + final String clientName = clientNamePrefix + i; + secureSettings.setString("s3.client." + clientName + ".role_arn", clientName + "_role_arn"); + secureSettings.setString("s3.client." + clientName + ".role_session_name", clientName + "_role_session_name"); + + // Use static AWS credentials for tests + secureSettings.setString("s3.client." + clientName + ".access_key", clientName + "_aws_access_key"); + secureSettings.setString("s3.client." + clientName + ".secret_key", clientName + "_aws_secret_key"); + + // Use explicit region setting + plainSettings.put("s3.client." + clientName + ".region", "us-east1"); + } + final Settings settings = Settings.builder().loadFromMap(plainSettings).setSecureSettings(secureSettings).build(); + final Map allClientsSettings = S3ClientSettings.load(settings); + // no less, no more + assertThat(allClientsSettings.size(), is(clientsCount + 1)); // including default + for (int i = 0; i < clientsCount; i++) { + final String clientName = clientNamePrefix + i; + final S3ClientSettings someClientSettings = allClientsSettings.get(clientName); + final AWSCredentialsProvider credentialsProvider = S3Service.buildCredentials(logger, someClientSettings); + assertThat(credentialsProvider, instanceOf(S3Service.PrivilegedSTSAssumeRoleSessionCredentialsProvider.class)); + ((Closeable) credentialsProvider).close(); + } + // test default exists and is an Instance provider + final S3ClientSettings defaultClientSettings = allClientsSettings.get("default"); + final AWSCredentialsProvider defaultCredentialsProvider = S3Service.buildCredentials(logger, defaultClientSettings); + assertThat(defaultCredentialsProvider, instanceOf(S3Service.PrivilegedInstanceProfileCredentialsProvider.class)); + } + + public void testIrsaCredentialsFromKeystore() throws IOException { + final Map plainSettings = new HashMap<>(); + final MockSecureSettings secureSettings = new MockSecureSettings(); + final String clientNamePrefix = "some_client_name_"; + final int clientsCount = randomIntBetween(0, 4); + for (int i = 0; i < clientsCount; i++) { + final String clientName = clientNamePrefix + i; + secureSettings.setString("s3.client." + clientName + ".role_arn", clientName + "_role_arn"); + secureSettings.setString("s3.client." + clientName + ".role_session_name", clientName + "_role_session_name"); + } + final Settings settings = Settings.builder().loadFromMap(plainSettings).setSecureSettings(secureSettings).build(); + final Map allClientsSettings = S3ClientSettings.load(settings); + // no less, no more + assertThat(allClientsSettings.size(), is(clientsCount + 1)); // including default + for (int i = 0; i < clientsCount; i++) { + final String clientName = clientNamePrefix + i; + final S3ClientSettings someClientSettings = allClientsSettings.get(clientName); + final AWSCredentialsProvider credentialsProvider = S3Service.buildCredentials(logger, someClientSettings); + assertThat(credentialsProvider, instanceOf(S3Service.PrivilegedSTSAssumeRoleSessionCredentialsProvider.class)); + ((Closeable) credentialsProvider).close(); + } + // test default exists and is an Instance provider + final S3ClientSettings defaultClientSettings = allClientsSettings.get("default"); + final AWSCredentialsProvider defaultCredentialsProvider = S3Service.buildCredentials(logger, defaultClientSettings); + assertThat(defaultCredentialsProvider, instanceOf(S3Service.PrivilegedInstanceProfileCredentialsProvider.class)); + } + public void testSetDefaultCredential() { final MockSecureSettings secureSettings = new MockSecureSettings(); final String awsAccessKey = randomAlphaOfLength(8); diff --git a/plugins/repository-s3/src/test/java/org/opensearch/repositories/s3/RepositoryCredentialsTests.java b/plugins/repository-s3/src/test/java/org/opensearch/repositories/s3/RepositoryCredentialsTests.java index 9c359d67db88b..a30b36cdd659c 100644 --- a/plugins/repository-s3/src/test/java/org/opensearch/repositories/s3/RepositoryCredentialsTests.java +++ b/plugins/repository-s3/src/test/java/org/opensearch/repositories/s3/RepositoryCredentialsTests.java @@ -317,9 +317,10 @@ public static final class ProxyS3Service extends S3Service { private static final Logger logger = LogManager.getLogger(ProxyS3Service.class); @Override - AmazonS3 buildClient(final S3ClientSettings clientSettings) { - final AmazonS3 client = super.buildClient(clientSettings); - return new ClientAndCredentials(client, buildCredentials(logger, clientSettings)); + AmazonS3WithCredentials buildClient(final S3ClientSettings clientSettings) { + final AmazonS3WithCredentials client = super.buildClient(clientSettings); + final AWSCredentialsProvider credentials = buildCredentials(logger, clientSettings); + return AmazonS3WithCredentials.create(new ClientAndCredentials(client.client(), credentials), credentials); } } diff --git a/plugins/repository-s3/src/test/java/org/opensearch/repositories/s3/S3ClientSettingsTests.java b/plugins/repository-s3/src/test/java/org/opensearch/repositories/s3/S3ClientSettingsTests.java index 462ed5377ff9a..a86ed3af17476 100644 --- a/plugins/repository-s3/src/test/java/org/opensearch/repositories/s3/S3ClientSettingsTests.java +++ b/plugins/repository-s3/src/test/java/org/opensearch/repositories/s3/S3ClientSettingsTests.java @@ -45,6 +45,7 @@ import java.util.Locale; import java.util.Map; +import static org.hamcrest.CoreMatchers.startsWith; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.emptyString; import static org.hamcrest.Matchers.is; @@ -118,6 +119,52 @@ public void testRejectionOfLoneSessionToken() { assertThat(e.getMessage(), is("Missing access key and secret key for s3 client [default]")); } + public void testIrsaCredentialsTypeWithIdentityTokenFile() { + final Map settings = S3ClientSettings.load( + Settings.builder().put("s3.client.default.identity_token_file", "file").build() + ); + final S3ClientSettings defaultSettings = settings.get("default"); + final S3ClientSettings.IrsaCredentials credentials = defaultSettings.irsaCredentials; + assertThat(credentials.getIdentityTokenFile(), is("file")); + assertThat(credentials.getRoleArn(), is(nullValue())); + assertThat(credentials.getRoleSessionName(), startsWith("s3-sdk-java-")); + } + + public void testIrsaCredentialsTypeRoleArn() { + final MockSecureSettings secureSettings = new MockSecureSettings(); + secureSettings.setString("s3.client.default.role_arn", "role"); + final Map settings = S3ClientSettings.load(Settings.builder().setSecureSettings(secureSettings).build()); + final S3ClientSettings defaultSettings = settings.get("default"); + final S3ClientSettings.IrsaCredentials credentials = defaultSettings.irsaCredentials; + assertThat(credentials.getRoleArn(), is("role")); + assertThat(credentials.getRoleSessionName(), startsWith("s3-sdk-java-")); + } + + public void testIrsaCredentialsTypeWithRoleArnAndRoleSessionName() { + final MockSecureSettings secureSettings = new MockSecureSettings(); + secureSettings.setString("s3.client.default.role_arn", "role"); + secureSettings.setString("s3.client.default.role_session_name", "session"); + final Map settings = S3ClientSettings.load(Settings.builder().setSecureSettings(secureSettings).build()); + final S3ClientSettings defaultSettings = settings.get("default"); + final S3ClientSettings.IrsaCredentials credentials = defaultSettings.irsaCredentials; + assertThat(credentials.getRoleArn(), is("role")); + assertThat(credentials.getRoleSessionName(), is("session")); + } + + public void testIrsaCredentialsTypeWithRoleArnAndRoleSessionNameAndIdentityTokeFile() { + final MockSecureSettings secureSettings = new MockSecureSettings(); + secureSettings.setString("s3.client.default.role_arn", "role"); + secureSettings.setString("s3.client.default.role_session_name", "session"); + final Map settings = S3ClientSettings.load( + Settings.builder().setSecureSettings(secureSettings).put("s3.client.default.identity_token_file", "file").build() + ); + final S3ClientSettings defaultSettings = settings.get("default"); + final S3ClientSettings.IrsaCredentials credentials = defaultSettings.irsaCredentials; + assertThat(credentials.getIdentityTokenFile(), is("file")); + assertThat(credentials.getRoleArn(), is("role")); + assertThat(credentials.getRoleSessionName(), is("session")); + } + public void testCredentialsTypeWithAccessKeyAndSecretKey() { final MockSecureSettings secureSettings = new MockSecureSettings(); secureSettings.setString("s3.client.default.access_key", "access_key"); @@ -199,7 +246,7 @@ public void testRegionCanBeSet() { assertThat(settings.get("default").region, is("")); assertThat(settings.get("other").region, is(region)); try (S3Service s3Service = new S3Service()) { - AmazonS3Client other = (AmazonS3Client) s3Service.buildClient(settings.get("other")); + AmazonS3Client other = (AmazonS3Client) s3Service.buildClient(settings.get("other")).client(); assertThat(other.getSignerRegionOverride(), is(region)); } } diff --git a/plugins/repository-s3/src/test/java/org/opensearch/repositories/s3/S3ServiceTests.java b/plugins/repository-s3/src/test/java/org/opensearch/repositories/s3/S3ServiceTests.java index cb0e76e272b4e..71e42907ab997 100644 --- a/plugins/repository-s3/src/test/java/org/opensearch/repositories/s3/S3ServiceTests.java +++ b/plugins/repository-s3/src/test/java/org/opensearch/repositories/s3/S3ServiceTests.java @@ -32,10 +32,12 @@ package org.opensearch.repositories.s3; import org.opensearch.cluster.metadata.RepositoryMetadata; - +import org.opensearch.common.settings.MockSecureSettings; import org.opensearch.common.settings.Settings; import org.opensearch.test.OpenSearchTestCase; +import java.util.Map; + public class S3ServiceTests extends OpenSearchTestCase { public void testCachedClientsAreReleased() { @@ -56,4 +58,29 @@ public void testCachedClientsAreReleased() { final S3ClientSettings clientSettingsReloaded = s3Service.settings(metadata1); assertNotSame(clientSettings, clientSettingsReloaded); } + + public void testCachedClientsWithCredentialsAreReleased() { + final MockSecureSettings secureSettings = new MockSecureSettings(); + secureSettings.setString("s3.client.default.role_arn", "role"); + final Map defaults = S3ClientSettings.load( + Settings.builder().setSecureSettings(secureSettings).put("s3.client.default.identity_token_file", "file").build() + ); + final S3Service s3Service = new S3Service(); + s3Service.refreshAndClearCache(defaults); + final Settings settings = Settings.builder().put("endpoint", "http://first").put("region", "us-east-2").build(); + final RepositoryMetadata metadata1 = new RepositoryMetadata("first", "s3", settings); + final RepositoryMetadata metadata2 = new RepositoryMetadata("second", "s3", settings); + final S3ClientSettings clientSettings = s3Service.settings(metadata2); + final S3ClientSettings otherClientSettings = s3Service.settings(metadata2); + assertSame(clientSettings, otherClientSettings); + final AmazonS3Reference reference = s3Service.client(metadata1); + reference.close(); + s3Service.close(); + final AmazonS3Reference referenceReloaded = s3Service.client(metadata1); + assertNotSame(referenceReloaded, reference); + referenceReloaded.close(); + s3Service.close(); + final S3ClientSettings clientSettingsReloaded = s3Service.settings(metadata1); + assertNotSame(clientSettings, clientSettingsReloaded); + } }