Skip to content

Commit 8e5bae6

Browse files
committed
feat: defer to node chain in cliv2compat
1 parent 46383e8 commit 8e5bae6

6 files changed

+133
-393
lines changed

packages/credential-providers/README.md

+42-52
Original file line numberDiff line numberDiff line change
@@ -827,7 +827,7 @@ Successfully signed out of all SSO profiles.
827827
### Sample files
828828

829829
This credential provider is only applicable if the profile specified in shared configuration and
830-
credentials files contain ALL of the following entries.
830+
credentials files contain ALL the following entries.
831831

832832
#### `~/.aws/credentials`
833833

@@ -950,7 +950,47 @@ new S3({
950950
});
951951
```
952952

953-
## `resolveAwsCliV2Region()`
953+
## `fromAwsCliV2CompatibleProviderChain()`
954+
955+
- Not available in browsers & native apps.
956+
957+
A credential provider that follows the same priority order as the AWS CLI v2 credential resolution.
958+
This credential provider will attempt to find credentials from the following sources (listed in
959+
order of precedence):
960+
961+
- Inline code static credentials
962+
- **when a profile is specified**:
963+
- Same resolution as the [fromIni](#fromini) provider.
964+
- **when a profile is not specified**:
965+
- [Environment variables exposed via `process.env`](#fromenv)
966+
- [Web identity token credentials](#fromtokenfile)
967+
- [SSO credentials from token cache](#fromsso)
968+
- [From Credential Process](#fromprocess)
969+
- [From Instance and Container Metadata Service](#fromcontainermetadata-and-frominstancemetadata)
970+
971+
```js
972+
import { fromAwsCliV2CompatibleProviderChain, resolveAwsCliV2Region } from "@aws-sdk/credential-providers";
973+
import { S3Client } from "@aws-sdk/client-s3";
974+
975+
const s3Client = new S3Client({
976+
profile: "my-profile",
977+
978+
// Implements AWS CLI v2-compatible credential resolution and proxy settings.
979+
credentials: fromAwsCliV2CompatibleProviderChain({}),
980+
981+
// Implements AWS CLI v2 region resolution logic.
982+
region: resolveAwsCliV2Region({
983+
// (!) this duplication is required if not using the "default" profile.
984+
profile: "my-profile",
985+
defaultRegion: "us-east-1", // optional
986+
}),
987+
});
988+
```
989+
990+
### `resolveAwsCliV2Region()`
991+
992+
- This is not a credential resolver. It is a region resolver included here for ease of access.
993+
- Not available in browsers & native apps.
954994

955995
The region is resolved using the following order of precedence (highest to lowest) in the cli v2.
956996

@@ -971,56 +1011,6 @@ The region is resolved using the following order of precedence (highest to lowes
9711011
- Uses provided default region if specified
9721012
- Returns undefined if no region can be determined
9731013

974-
Basic Usage
975-
976-
```
977-
import { resolveAwsCliV2Region } from "@aws-sdk/credential-providers";
978-
import { S3Client } from "@aws-sdk/client-s3";
979-
980-
const client = new S3Client({
981-
region: await resolveAwsCliV2Region({})
982-
});
983-
984-
```
985-
986-
## `fromAwsCliV2CompatibleProviderChain()`
987-
988-
A credential provider that follows the same priority chain as AWS CLI v2 for credential resolution.
989-
This credential provider will attempt to find credentials from the following sources (listed in
990-
order of precedence):
991-
992-
- Static credentials
993-
- [Shared credentials and config ini files](#fromini) when a profile is specified
994-
- [Environment variables exposed via `process.env`](#fromenv)
995-
- [Web identity token credentials](#fromtokenfile)
996-
- [SSO credentials from token cache](#fromsso)
997-
- [From Credential Process](#fromprocess)
998-
- [From Instance and Container Metadata Service](#fromcontainermetadata-and-frominstancemetadata)
999-
1000-
Example:
1001-
1002-
```
1003-
Import {
1004-
fromAwsCliV2CompatibleProviderChain,
1005-
resolveAwsCliV2Region
1006-
} from "@aws-sdk/credential-providers";
1007-
import { S3Client } from "@aws-sdk/client-s3";
1008-
1009-
const s3Client = new S3Client({
1010-
profile: 'application-profile',
1011-
1012-
// Implements AWS CLI-compatible credential resolution and proxy settings.
1013-
credentials: fromAwsCliV2CompatibleProviderChain({
1014-
proxyUrl: "http://localhost:8080", // Optional: Uses proxy settings.
1015-
certificateBundle: "/home/user/certificate.pem", // Optional: Custom CA bundle.
1016-
}),
1017-
1018-
// Implements AWS CLI region resolution logic.
1019-
region: resolveAwsCliV2Region(),
1020-
// Other configurations like retry strategy, logging, etc.
1021-
});
1022-
```
1023-
10241014
## Add Custom Headers to STS assume-role calls
10251015

10261016
You can specify the plugins--groups of middleware, to inject to the STS client.
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
1-
import { fromInstanceMetadata } from "@smithy/credential-provider-imds";
2-
import { CredentialsProviderError } from "@smithy/property-provider";
1+
import type { Exact } from "@smithy/types";
32
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
43

5-
import { fromAwsCliV2CompatibleProviderChain } from "./fromAwsCliV2CompatibleProviderChain";
4+
import {
5+
AwsCliV2CompatibleProviderOptions,
6+
fromAwsCliV2CompatibleProviderChain,
7+
} from "./fromAwsCliV2CompatibleProviderChain";
8+
9+
// the options type should have no required fields.
10+
type Assert = Exact<AwsCliV2CompatibleProviderOptions, Partial<AwsCliV2CompatibleProviderOptions>>;
11+
const typeAssertion: Assert = true as const;
12+
void typeAssertion;
613

714
describe("fromAwsCliV2CompatibleProviderChain", () => {
815
let mockFromIni: any;
9-
let mockFromEnv: any;
10-
let mockFromTokenFile: any;
11-
let mockFromSSO: any;
12-
let mockFromProcess: any;
13-
let mockFromInstanceMetadata: any;
16+
let mockFromNodeProviderChain: any;
1417

1518
const mockLogger = {
1619
debug: vi.fn(),
@@ -33,61 +36,14 @@ describe("fromAwsCliV2CompatibleProviderChain", () => {
3336
fromIni: mockFromIni,
3437
}));
3538

36-
mockFromEnv = vi.fn(async () => ({
37-
accessKeyId: "ENV_ACCESS_KEY",
38-
secretAccessKey: "ENV_SECRET_KEY",
39-
}));
40-
vi.doMock("@aws-sdk/credential-provider-env", () => ({
41-
fromEnv: () => mockFromEnv,
42-
}));
43-
mockFromEnv = vi.fn(async () => {
44-
return {
45-
accessKeyId: "ENV_ACCESS_KEY",
46-
secretAccessKey: "ENV_SECRET_KEY",
47-
};
48-
});
49-
vi.doMock("@aws-sdk/credential-provider-env", () => ({
50-
fromEnv: () => mockFromEnv,
51-
}));
52-
53-
mockFromTokenFile = vi.fn(async () => {
54-
return {
55-
accessKeyId: "TOKEN_ACCESS_KEY",
56-
secretAccessKey: "TOKEN_SECRET_KEY",
57-
};
58-
});
59-
vi.doMock("@aws-sdk/credential-provider-web-identity", () => ({
60-
fromTokenFile: () => mockFromTokenFile,
61-
}));
62-
63-
mockFromSSO = vi.fn(async () => {
64-
return {
65-
accessKeyId: "SSO_ACCESS_KEY",
66-
secretAccessKey: "SSO_SECRET_KEY",
67-
};
68-
});
69-
vi.doMock("@aws-sdk/credential-provider-sso", () => ({
70-
fromSSO: () => mockFromSSO,
71-
}));
72-
73-
mockFromProcess = vi.fn(async () => {
74-
return {
75-
accessKeyId: "PROCESS_ACCESS_KEY",
76-
secretAccessKey: "PROCESS_SECRET_KEY",
77-
};
78-
});
79-
vi.doMock("@aws-sdk/credential-provider-process", () => ({
80-
fromProcess: () => mockFromProcess,
81-
}));
82-
83-
mockFromInstanceMetadata = vi.fn(async () => {
84-
return {
85-
accessKeyId: "REMOTE_ACCESS_KEY",
86-
secretAccessKey: "REMOTE_SECRET_KEY",
87-
};
88-
});
89-
vi.doMock("@smithy/credential-provider-imds", () => ({
90-
fromInstanceMetadata: () => mockFromInstanceMetadata,
39+
mockFromNodeProviderChain = vi.fn(() =>
40+
vi.fn(async () => ({
41+
accessKeyId: "AWS_SDK_CHAIN_AK",
42+
secretAccessKey: "AWS_SDK_CHAIN_SK",
43+
}))
44+
);
45+
vi.doMock("@aws-sdk/credential-provider-node", () => ({
46+
defaultProvider: mockFromNodeProviderChain,
9147
}));
9248
});
9349

@@ -109,170 +65,22 @@ describe("fromAwsCliV2CompatibleProviderChain", () => {
10965
});
11066

11167
expect(mockFromIni).toHaveBeenCalled();
112-
expect(mockLogger.debug).toHaveBeenCalledWith(expect.stringContaining("Using fromIni with profile:test-profile"));
113-
});
114-
115-
it("should fall back to environment variables if no profile is provided", async () => {
116-
const provider = fromAwsCliV2CompatibleProviderChain({
117-
logger: mockLogger,
118-
});
119-
120-
const result = await provider();
121-
122-
expect(result).toEqual({
123-
accessKeyId: "ENV_ACCESS_KEY",
124-
secretAccessKey: "ENV_SECRET_KEY",
125-
});
126-
127-
expect(mockFromEnv).toHaveBeenCalled();
128-
expect(mockLogger.debug).toHaveBeenCalledWith(expect.stringContaining("Using from custom credential chain"));
129-
});
130-
131-
it("should fall back to web identity credentials when environment variables are unavailable", async () => {
132-
vi.doMock("@aws-sdk/credential-provider-env", () => ({
133-
fromEnv: () => () => Promise.reject(new CredentialsProviderError("No env credentials")),
134-
}));
135-
136-
const provider = fromAwsCliV2CompatibleProviderChain({ logger: mockLogger });
137-
138-
const result = await provider();
139-
140-
expect(result).toEqual({
141-
accessKeyId: "TOKEN_ACCESS_KEY",
142-
secretAccessKey: "TOKEN_SECRET_KEY",
143-
});
144-
145-
expect(mockFromTokenFile).toHaveBeenCalled();
146-
});
147-
148-
it("should fall back to SSO credentials when web identity is unavailable", async () => {
149-
vi.doMock("@aws-sdk/credential-provider-env", () => ({
150-
fromEnv: () => () => Promise.reject(new CredentialsProviderError("No env credentials")),
151-
}));
152-
vi.doMock("@aws-sdk/credential-provider-web-identity", () => ({
153-
fromTokenFile: () => () => Promise.reject(new CredentialsProviderError("No token file")),
154-
}));
155-
156-
const provider = fromAwsCliV2CompatibleProviderChain({ logger: mockLogger });
157-
158-
const result = await provider();
159-
160-
expect(result).toEqual({
161-
accessKeyId: "SSO_ACCESS_KEY",
162-
secretAccessKey: "SSO_SECRET_KEY",
163-
});
164-
165-
expect(mockFromSSO).toHaveBeenCalled();
166-
});
167-
168-
it("should fall back to process credentials when SSO is unavailable", async () => {
169-
vi.doMock("@aws-sdk/credential-provider-env", () => ({
170-
fromEnv: () => () => Promise.reject(new CredentialsProviderError("No env credentials")),
171-
}));
172-
173-
vi.doMock("@aws-sdk/credential-provider-ini", () => ({
174-
fromIni: () => () => Promise.reject(new CredentialsProviderError("No ini credentials")),
175-
}));
176-
177-
vi.doMock("@aws-sdk/credential-provider-web-identity", () => ({
178-
fromTokenFile: () => () => Promise.reject(new CredentialsProviderError("No token file")),
179-
}));
180-
181-
vi.doMock("@aws-sdk/credential-provider-sso", () => ({
182-
fromSSO: () => () => Promise.reject(new CredentialsProviderError("No SSO credentials")),
183-
}));
184-
185-
mockFromProcess = vi.fn().mockResolvedValue({
186-
accessKeyId: "PROCESS_ACCESS_KEY",
187-
secretAccessKey: "PROCESS_SECRET_KEY",
188-
});
189-
190-
vi.doMock("@aws-sdk/credential-provider-process", () => ({
191-
fromProcess: () => mockFromProcess,
192-
}));
193-
194-
vi.doMock("@smithy/credential-provider-imds", () => ({
195-
fromInstanceMetadata: () => mockFromInstanceMetadata,
196-
}));
197-
198-
const { fromAwsCliV2CompatibleProviderChain } = await import("./fromAwsCliV2CompatibleProviderChain");
199-
const provider = fromAwsCliV2CompatibleProviderChain({
200-
logger: mockLogger,
201-
});
202-
203-
const result = await provider();
204-
205-
expect(result).toEqual({
206-
accessKeyId: "PROCESS_ACCESS_KEY",
207-
secretAccessKey: "PROCESS_SECRET_KEY",
208-
});
209-
expect(mockFromProcess).toHaveBeenCalled();
68+
expect(mockLogger.debug).toHaveBeenCalledWith(
69+
`@aws-sdk/credential-providers - fromAwsCliV2CompatibleProviderChain - Using fromIni with profile: test-profile`
70+
);
21071
});
21172

212-
it("should fall back to remote credentials when process credentials are unavailable", async () => {
213-
vi.doMock("@aws-sdk/credential-provider-env", () => ({
214-
fromEnv: () => () => Promise.reject(new CredentialsProviderError("No env credentials")),
215-
}));
216-
217-
vi.doMock("@aws-sdk/credential-provider-ini", () => ({
218-
fromIni: () => () => Promise.reject(new CredentialsProviderError("No ini credentials")),
219-
}));
220-
221-
vi.doMock("@aws-sdk/credential-provider-web-identity", () => ({
222-
fromTokenFile: () => () => Promise.reject(new CredentialsProviderError("No token file")),
223-
}));
224-
225-
vi.doMock("@aws-sdk/credential-provider-sso", () => ({
226-
fromSSO: () => () => Promise.reject(new CredentialsProviderError("No SSO credentials")),
227-
}));
228-
229-
vi.doMock("@aws-sdk/credential-provider-process", () => ({
230-
fromProcess: () => () => Promise.reject(new CredentialsProviderError("No process credentials")),
231-
}));
232-
233-
mockFromInstanceMetadata = vi.fn().mockResolvedValue({
234-
accessKeyId: "REMOTE_ACCESS_KEY",
235-
secretAccessKey: "REMOTE_SECRET_KEY",
236-
});
237-
238-
vi.doMock("@smithy/credential-provider-imds", () => ({
239-
fromInstanceMetadata: () => mockFromInstanceMetadata,
240-
}));
241-
242-
const { fromAwsCliV2CompatibleProviderChain } = await import("./fromAwsCliV2CompatibleProviderChain");
73+
it.only("should fall back to fromNodeProviderChain when no profile is specified", async () => {
24374
const provider = fromAwsCliV2CompatibleProviderChain({
244-
logger: mockLogger,
75+
logger: console,
24576
});
24677

24778
const result = await provider();
24879

24980
expect(result).toEqual({
250-
accessKeyId: "REMOTE_ACCESS_KEY",
251-
secretAccessKey: "REMOTE_SECRET_KEY",
81+
accessKeyId: "AWS_SDK_CHAIN_AK",
82+
secretAccessKey: "AWS_SDK_CHAIN_SK",
25283
});
253-
expect(mockFromInstanceMetadata).toHaveBeenCalled();
254-
});
255-
256-
it("should throw error when no credentials are found", async () => {
257-
vi.doMock("@aws-sdk/credential-provider-env", () => ({
258-
fromEnv: () => () => Promise.reject(new CredentialsProviderError("No env credentials")),
259-
}));
260-
vi.doMock("@aws-sdk/credential-provider-web-identity", () => ({
261-
fromTokenFile: () => () => Promise.reject(new CredentialsProviderError("No token file")),
262-
}));
263-
vi.doMock("@aws-sdk/credential-provider-sso", () => ({
264-
fromSSO: () => () => Promise.reject(new CredentialsProviderError("No SSO credentials")),
265-
}));
266-
vi.doMock("@aws-sdk/credential-provider-process", () => ({
267-
fromProcess: () => () => Promise.reject(new CredentialsProviderError("No process credentials")),
268-
}));
269-
vi.doMock("@smithy/credential-provider-imds", () => ({
270-
fromInstanceMetadata: () => () => Promise.reject(new CredentialsProviderError("No remote credentials")),
271-
}));
272-
const { fromAwsCliV2CompatibleProviderChain } = await import("./fromAwsCliV2CompatibleProviderChain");
273-
const provider = fromAwsCliV2CompatibleProviderChain({ logger: mockLogger });
274-
275-
await expect(provider()).rejects.toThrow(CredentialsProviderError);
276-
await expect(provider()).rejects.toThrow("Could not load credentials from any providers");
84+
expect(mockFromNodeProviderChain).toHaveBeenCalled();
27785
});
27886
});

0 commit comments

Comments
 (0)