Skip to content

Commit 7b8fd06

Browse files
committed
Adds account ID support for profile credentials provider sources (#4340)
1 parent 772d170 commit 7b8fd06

File tree

8 files changed

+306
-79
lines changed

8 files changed

+306
-79
lines changed

core/auth/src/main/java/software/amazon/awssdk/auth/credentials/ProcessCredentialsProvider.java

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ public final class ProcessCredentialsProvider
7272
private final List<String> executableCommand;
7373
private final Duration credentialRefreshThreshold;
7474
private final long processOutputLimit;
75+
private final String staticAccountId;
7576

7677
private final CachedSupplier<AwsCredentials> processCredentialCache;
7778

@@ -91,6 +92,7 @@ private ProcessCredentialsProvider(Builder builder) {
9192
this.commandFromBuilder = builder.command;
9293
this.commandAsListOfStringsFromBuilder = builder.commandAsListOfStrings;
9394
this.asyncCredentialUpdateEnabled = builder.asyncCredentialUpdateEnabled;
95+
this.staticAccountId = builder.staticAccountId;
9496

9597
CachedSupplier.Builder<AwsCredentials> cacheBuilder = CachedSupplier.builder(this::refreshCredentials)
9698
.cachedValueName(toString());
@@ -181,19 +183,21 @@ private AwsCredentials credentials(JsonNode credentialsJson) {
181183
Validate.notEmpty(accessKeyId, "AccessKeyId cannot be empty.");
182184
Validate.notEmpty(secretAccessKey, "SecretAccessKey cannot be empty.");
183185

186+
String resolvedAccountId = accountId == null ? this.staticAccountId : accountId;
187+
184188
return sessionToken != null ?
185189
AwsSessionCredentials.builder()
186190
.accessKeyId(accessKeyId)
187191
.secretAccessKey(secretAccessKey)
188192
.sessionToken(sessionToken)
189193
.expirationTime(credentialExpirationTime(credentialsJson))
190-
.accountId(accountId)
194+
.accountId(resolvedAccountId)
191195
.providerName(PROVIDER_NAME)
192196
.build() :
193197
AwsBasicCredentials.builder()
194198
.accessKeyId(accessKeyId)
195199
.secretAccessKey(secretAccessKey)
196-
.accountId(accountId)
200+
.accountId(resolvedAccountId)
197201
.providerName(PROVIDER_NAME)
198202
.build();
199203
}
@@ -265,6 +269,7 @@ public static class Builder implements CopyableBuilder<Builder, ProcessCredentia
265269
private List<String> commandAsListOfStrings;
266270
private Duration credentialRefreshThreshold = Duration.ofSeconds(15);
267271
private long processOutputLimit = 64000;
272+
private String staticAccountId;
268273

269274
/**
270275
* @see #builder()
@@ -278,6 +283,7 @@ private Builder(ProcessCredentialsProvider provider) {
278283
this.commandAsListOfStrings = provider.commandAsListOfStringsFromBuilder;
279284
this.credentialRefreshThreshold = provider.credentialRefreshThreshold;
280285
this.processOutputLimit = provider.processOutputLimit;
286+
this.staticAccountId = provider.staticAccountId;
281287
}
282288

283289
/**
@@ -338,6 +344,19 @@ public Builder processOutputLimit(long outputByteLimit) {
338344
return this;
339345
}
340346

347+
/**
348+
* Configure a static account id for this credentials provider. Account id for ProcessCredentialsProvider is only
349+
* relevant in a context where a service constructs endpoint URL containing an account id.
350+
* This option should ONLY be used if the provider should return credentials with account id, and the process does not
351+
* output account id. If a static account ID is configured, and the process also returns an account
352+
* id, the process output value overrides the static value. If used, the static account id MUST match the credentials
353+
* returned by the process.
354+
*/
355+
public Builder staticAccountId(String staticAccountId) {
356+
this.staticAccountId = staticAccountId;
357+
return this;
358+
}
359+
341360
public ProcessCredentialsProvider build() {
342361
return new ProcessCredentialsProvider(this);
343362
}

core/auth/src/main/java/software/amazon/awssdk/auth/credentials/internal/ProfileCredentialsUtils.java

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -157,8 +157,11 @@ private Optional<AwsCredentialsProvider> credentialsProvider(Set<String> childre
157157
private AwsCredentialsProvider basicProfileCredentialsProvider() {
158158
requireProperties(ProfileProperty.AWS_ACCESS_KEY_ID,
159159
ProfileProperty.AWS_SECRET_ACCESS_KEY);
160-
AwsCredentials credentials = AwsBasicCredentials.create(properties.get(ProfileProperty.AWS_ACCESS_KEY_ID),
161-
properties.get(ProfileProperty.AWS_SECRET_ACCESS_KEY));
160+
AwsCredentials credentials = AwsBasicCredentials.builder()
161+
.accessKeyId(properties.get(ProfileProperty.AWS_ACCESS_KEY_ID))
162+
.secretAccessKey(properties.get(ProfileProperty.AWS_SECRET_ACCESS_KEY))
163+
.accountId(properties.get(ProfileProperty.AWS_ACCOUNT_ID))
164+
.build();
162165
return StaticCredentialsProvider.create(credentials);
163166
}
164167

@@ -169,9 +172,12 @@ private AwsCredentialsProvider sessionProfileCredentialsProvider() {
169172
requireProperties(ProfileProperty.AWS_ACCESS_KEY_ID,
170173
ProfileProperty.AWS_SECRET_ACCESS_KEY,
171174
ProfileProperty.AWS_SESSION_TOKEN);
172-
AwsCredentials credentials = AwsSessionCredentials.create(properties.get(ProfileProperty.AWS_ACCESS_KEY_ID),
173-
properties.get(ProfileProperty.AWS_SECRET_ACCESS_KEY),
174-
properties.get(ProfileProperty.AWS_SESSION_TOKEN));
175+
AwsCredentials credentials = AwsSessionCredentials.builder()
176+
.accessKeyId(properties.get(ProfileProperty.AWS_ACCESS_KEY_ID))
177+
.secretAccessKey(properties.get(ProfileProperty.AWS_SECRET_ACCESS_KEY))
178+
.sessionToken(properties.get(ProfileProperty.AWS_SESSION_TOKEN))
179+
.accountId(properties.get(ProfileProperty.AWS_ACCOUNT_ID))
180+
.build();
175181
return StaticCredentialsProvider.create(credentials);
176182
}
177183

@@ -180,6 +186,7 @@ private AwsCredentialsProvider credentialProcessCredentialsProvider() {
180186

181187
return ProcessCredentialsProvider.builder()
182188
.command(properties.get(ProfileProperty.CREDENTIAL_PROCESS))
189+
.staticAccountId(properties.get(ProfileProperty.AWS_ACCOUNT_ID))
183190
.build();
184191
}
185192

core/auth/src/test/java/software/amazon/awssdk/auth/credentials/ProcessCredentialsProviderTest.java

Lines changed: 77 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,6 @@
1717
import static org.assertj.core.api.Assertions.assertThat;
1818
import static org.assertj.core.api.Assertions.assertThatThrownBy;
1919
import static org.assertj.core.api.Assertions.within;
20-
import static software.amazon.awssdk.auth.credentials.internal.ProcessCredentialsTestUtils.copyErrorCaseProcessCredentialsScript;
21-
import static software.amazon.awssdk.auth.credentials.internal.ProcessCredentialsTestUtils.copyHappyCaseProcessCredentialsScript;
2220

2321
import java.io.File;
2422
import java.io.FileOutputStream;
@@ -30,15 +28,19 @@
3028
import java.time.Instant;
3129
import java.time.temporal.ChronoUnit;
3230
import java.util.Arrays;
31+
import java.util.List;
3332
import java.util.Optional;
3433
import org.junit.jupiter.api.AfterAll;
3534
import org.junit.jupiter.api.BeforeAll;
3635
import org.junit.jupiter.api.Test;
36+
import org.junit.jupiter.params.ParameterizedTest;
37+
import org.junit.jupiter.params.provider.Arguments;
38+
import org.junit.jupiter.params.provider.MethodSource;
3739
import software.amazon.awssdk.utils.DateUtils;
3840
import software.amazon.awssdk.utils.IoUtils;
3941
import software.amazon.awssdk.utils.Platform;
4042

41-
class ProcessCredentialsProviderTest {
43+
public class ProcessCredentialsProviderTest {
4244

4345
private static final String PROCESS_RESOURCE_PATH = "/resources/process/";
4446
private static final String RANDOM_SESSION_TOKEN = "RANDOM_TOKEN";
@@ -52,13 +54,13 @@ class ProcessCredentialsProviderTest {
5254
private static String errorScriptLocation;
5355

5456
@BeforeAll
55-
public static void setup() {
57+
static void setup() {
5658
scriptLocation = copyHappyCaseProcessCredentialsScript();
5759
errorScriptLocation = copyErrorCaseProcessCredentialsScript();
5860
}
5961

6062
@AfterAll
61-
public static void teardown() {
63+
static void teardown() {
6264
if (scriptLocation != null && !new File(scriptLocation).delete()) {
6365
throw new IllegalStateException("Failed to delete file: " + scriptLocation);
6466
}
@@ -68,14 +70,47 @@ public static void teardown() {
6870
}
6971
}
7072

73+
@ParameterizedTest(name = "{index} - {0}")
74+
@MethodSource("staticCredentialsValues")
75+
void staticCredentialsCanBeLoaded(String description, String staticAccountId, Optional<String> expectedValue,
76+
String cmd) {
77+
ProcessCredentialsProvider.Builder providerBuilder = ProcessCredentialsProvider.builder().command(cmd);
78+
if (staticAccountId != null) {
79+
providerBuilder.staticAccountId(staticAccountId);
80+
}
81+
AwsCredentials credentials = providerBuilder.build().resolveCredentials();
82+
83+
verifyCredentials(credentials);
84+
assertThat(credentials).isNotInstanceOf(AwsSessionCredentials.class);
85+
86+
if (expectedValue.isPresent()) {
87+
assertThat(credentials.accountId()).isPresent().hasValue(expectedValue.get());
88+
} else {
89+
assertThat(credentials.accountId()).isNotPresent();
90+
}
91+
}
92+
93+
private static List<Arguments> staticCredentialsValues() {
94+
return Arrays.asList(
95+
Arguments.of("when only containing access key id, secret", null, Optional.empty(),
96+
String.format("%s accessKeyId secretAccessKey", scriptLocation)),
97+
Arguments.of("when output has account id", null, Optional.of(ACCOUNT_ID),
98+
String.format("%s %s %s acctid=%s", scriptLocation, ACCESS_KEY_ID, SECRET_ACCESS_KEY, ACCOUNT_ID)),
99+
Arguments.of("when output has account id, static account id configured", "staticAccountId", Optional.of(ACCOUNT_ID),
100+
String.format("%s %s %s acctid=%s", scriptLocation, ACCESS_KEY_ID, SECRET_ACCESS_KEY, ACCOUNT_ID)),
101+
Arguments.of("when only static account id is configured", "staticAccountId", Optional.of("staticAccountId"),
102+
String.format("%s %s %s", scriptLocation, ACCESS_KEY_ID, SECRET_ACCESS_KEY))
103+
);
104+
}
105+
71106
@Test
72107
void staticCredentialsCanBeLoaded() {
73108
AwsCredentials credentials =
74-
ProcessCredentialsProvider.builder()
75-
.command(String.format("%s accessKeyId secretAccessKey", scriptLocation))
76-
.build()
77-
.resolveCredentials();
78-
109+
ProcessCredentialsProvider.builder()
110+
.command(String.format("%s accessKeyId secretAccessKey", scriptLocation))
111+
.build()
112+
.resolveCredentials();
113+
79114
assertThat(credentials).isNotInstanceOf(AwsSessionCredentials.class);
80115
assertThat(credentials.accessKeyId()).isEqualTo(ACCESS_KEY_ID);
81116
assertThat(credentials.secretAccessKey()).isEqualTo(SECRET_ACCESS_KEY);
@@ -110,17 +145,17 @@ public void staticCredentials_commandAsListOfStrings_CanBeLoaded() {
110145
assertThat(credentials.secretAccessKey()).isEqualTo("secretAccessKey");
111146
assertThat(credentials.providerName()).isPresent().contains("ProcessCredentialsProvider");
112147
}
113-
148+
114149
@Test
115150
void sessionCredentialsCanBeLoaded() {
116151
String expiration = DateUtils.formatIso8601Date(Instant.now());
117152
ProcessCredentialsProvider credentialsProvider =
118-
ProcessCredentialsProvider.builder()
119-
.command(String.format("%s %s %s token=%s exp=%s",
120-
scriptLocation, ACCESS_KEY_ID, SECRET_ACCESS_KEY,
121-
SESSION_TOKEN, expiration))
122-
.credentialRefreshThreshold(Duration.ofSeconds(1))
123-
.build();
153+
ProcessCredentialsProvider.builder()
154+
.command(String.format("%s %s %s token=%s exp=%s",
155+
scriptLocation, ACCESS_KEY_ID, SECRET_ACCESS_KEY,
156+
SESSION_TOKEN, expiration))
157+
.credentialRefreshThreshold(Duration.ofSeconds(1))
158+
.build();
124159

125160
AwsCredentials credentials = credentialsProvider.resolveCredentials();
126161
verifySessionCredentials(credentials, expiration);
@@ -142,18 +177,39 @@ void sessionCredentialsWithAccountIdCanBeLoaded() {
142177
assertThat(credentials.accountId()).isPresent().isEqualTo(Optional.of(ACCOUNT_ID));
143178
}
144179

180+
@Test
181+
void sessionCredentialsWithStaticAccountIdCanBeLoaded() {
182+
String expiration = DateUtils.formatIso8601Date(Instant.now());
183+
ProcessCredentialsProvider credentialsProvider =
184+
ProcessCredentialsProvider.builder()
185+
.command(String.format("%s %s %s token=sessionToken exp=%s",
186+
scriptLocation, ACCESS_KEY_ID, SECRET_ACCESS_KEY, expiration))
187+
.credentialRefreshThreshold(Duration.ofSeconds(1))
188+
.staticAccountId("staticAccountId")
189+
.build();
190+
191+
AwsCredentials credentials = credentialsProvider.resolveCredentials();
192+
verifySessionCredentials(credentials, expiration);
193+
assertThat(credentials.accountId()).isPresent().hasValue("staticAccountId");
194+
}
195+
145196
private void verifySessionCredentials(AwsCredentials credentials, String expiration) {
197+
verifyCredentials(credentials);
198+
146199
assertThat(credentials).isInstanceOf(AwsSessionCredentials.class);
147200
AwsSessionCredentials sessionCredentials = (AwsSessionCredentials) credentials;
148-
149-
assertThat(sessionCredentials.accessKeyId()).isEqualTo(ACCESS_KEY_ID);
150-
assertThat(sessionCredentials.secretAccessKey()).isEqualTo(SECRET_ACCESS_KEY);
151201
assertThat(sessionCredentials.sessionToken()).isEqualTo(SESSION_TOKEN);
202+
152203
assertThat(sessionCredentials.expirationTime()).isPresent();
153204
Instant exp = sessionCredentials.expirationTime().get();
154205
assertThat(exp).isCloseTo(expiration, within(1, ChronoUnit.MICROS));
155206
}
156207

208+
private void verifyCredentials(AwsCredentials credentials) {
209+
assertThat(credentials.accessKeyId()).isEqualTo(ACCESS_KEY_ID);
210+
assertThat(credentials.secretAccessKey()).isEqualTo(SECRET_ACCESS_KEY);
211+
}
212+
157213
@Test
158214
void resultsAreCached() {
159215
ProcessCredentialsProvider credentialsProvider =
@@ -212,7 +268,7 @@ void lackOfExpirationIsCachedForever() {
212268

213269
assertThat(request1).isEqualTo(request2);
214270
}
215-
271+
216272
@Test
217273
public void processOutputLimitIsEnforced() {
218274
ProcessCredentialsProvider credentialsProvider =
@@ -228,7 +284,6 @@ public void processOutputLimitIsEnforced() {
228284

229285
@Test
230286
void processOutputLimitDefaultPassesLargeInput() {
231-
232287
String longSessionToken = "lYzvmByqdS1E69QQVEavDDHabQ2GuYKYABKRA4xLbAXpdnFtV030UH4" +
233288
"bQoZWCDcfADFvBwBm3ixEFTYMjn5XQozpFV2QAsWHirCVcEJ5DC60KPCNBcDi4KLNJfbsp3r6kKTOmYOeqhEyiC4emDX33X2ppZsa5" +
234289
"1iwr6ShIZPOUPmuR4WDglmWubgO2q5tZv48xA5idkcHEmtGdoL343sY24q4gMh21eeBnF6ikjZdfvZ0Mn86UQ8r05AD346rSwM5bFs" +

core/auth/src/test/java/software/amazon/awssdk/auth/credentials/ProfileCredentialsProviderTest.java

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import java.nio.file.FileSystem;
2525
import java.nio.file.Files;
2626
import java.nio.file.Path;
27+
import java.util.Optional;
2728
import java.util.function.Supplier;
2829
import org.junit.jupiter.api.AfterAll;
2930
import org.junit.jupiter.api.BeforeAll;
@@ -114,6 +115,24 @@ void presentProfileReturnsCredentials() {
114115
assertThat(provider.resolveCredentials()).satisfies(credentials -> {
115116
assertThat(credentials.accessKeyId()).isEqualTo("defaultAccessKey");
116117
assertThat(credentials.secretAccessKey()).isEqualTo("defaultSecretAccessKey");
118+
assertThat(credentials.accountId()).isNotPresent();
119+
});
120+
}
121+
122+
@Test
123+
void presentProfileWithAccountIdReturnsCredentialsWithAccountId() {
124+
ProfileFile file = profileFile("[default]\n"
125+
+ "aws_access_key_id = defaultAccessKey\n"
126+
+ "aws_secret_access_key = defaultSecretAccessKey\n"
127+
+ "aws_account_id = defaultAccountId");
128+
129+
ProfileCredentialsProvider provider =
130+
ProfileCredentialsProvider.builder().profileFile(file).profileName("default").build();
131+
132+
assertThat(provider.resolveCredentials()).satisfies(credentials -> {
133+
assertThat(credentials.accessKeyId()).isEqualTo("defaultAccessKey");
134+
assertThat(credentials.secretAccessKey()).isEqualTo("defaultSecretAccessKey");
135+
assertThat(credentials.accountId()).isPresent().isEqualTo(Optional.of("defaultAccountId"));
117136
});
118137
}
119138

@@ -201,13 +220,15 @@ void resolveCredentials_presentProfileFileSupplier_returnsCredentials() {
201220
assertThat(provider.resolveCredentials()).satisfies(credentials -> {
202221
assertThat(credentials.accessKeyId()).isEqualTo("defaultAccessKey");
203222
assertThat(credentials.secretAccessKey()).isEqualTo("defaultSecretAccessKey");
223+
assertThat(credentials.accountId()).isNotPresent();
204224
});
205225
}
206226

207227
@Test
208228
void resolveCredentials_presentSupplierProfileFile_returnsCredentials() {
209229
Supplier<ProfileFile> supplier = () -> profileFile("[default]\naws_access_key_id = defaultAccessKey\n"
210-
+ "aws_secret_access_key = defaultSecretAccessKey\n");
230+
+ "aws_secret_access_key = defaultSecretAccessKey\n"
231+
+ "aws_account_id = defaultAccountId");
211232

212233
ProfileCredentialsProvider provider =
213234
ProfileCredentialsProvider.builder()
@@ -218,6 +239,7 @@ void resolveCredentials_presentSupplierProfileFile_returnsCredentials() {
218239
assertThat(provider.resolveCredentials()).satisfies(credentials -> {
219240
assertThat(credentials.accessKeyId()).isEqualTo("defaultAccessKey");
220241
assertThat(credentials.secretAccessKey()).isEqualTo("defaultSecretAccessKey");
242+
assertThat(credentials.accountId()).isPresent();
221243
});
222244
}
223245

0 commit comments

Comments
 (0)