feat: support custom STS endpoints (#1762)

Closes #1067. This is a advanced option
and is not needed for most deployments.
This commit is contained in:
Tom Keller
2026-05-07 14:50:17 -07:00
committed by GitHub
parent 681892c11b
commit 8d52d05d7a
6 changed files with 201 additions and 117 deletions
+49 -3
View File
@@ -311,9 +311,7 @@ describe('Configure AWS Credentials', {}, () => {
});
process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN = 'fake-token';
await run();
expect(core.warning).toHaveBeenCalledWith(
expect.stringContaining("'custom-tags' is set but will be ignored"),
);
expect(core.warning).toHaveBeenCalledWith(expect.stringContaining("'custom-tags' is set but will be ignored"));
});
});
@@ -801,6 +799,54 @@ describe('Configure AWS Credentials', {}, () => {
});
});
describe('Custom STS Endpoint', {}, () => {
it('passes sts-endpoint to the STS client', async () => {
const client = new CredentialsClient({
region: 'us-east-1',
stsEndpoint: 'https://sts.custom.example.com',
roleChaining: false,
});
const endpoint = await client.stsClient.config.endpoint();
expect(endpoint).toMatchObject({ hostname: 'sts.custom.example.com', protocol: 'https:' });
});
it('does not override endpoint when sts-endpoint is not provided', () => {
const client = new CredentialsClient({
region: 'us-east-1',
roleChaining: false,
});
expect(client.stsClient.config.endpoint).toBeUndefined();
});
it('works with http endpoints for local services', async () => {
const client = new CredentialsClient({
region: 'us-east-1',
stsEndpoint: 'http://localhost:9000',
roleChaining: false,
});
const endpoint = await client.stsClient.config.endpoint();
expect(endpoint).toMatchObject({ hostname: 'localhost', protocol: 'http:', port: 9000 });
});
it('succeeds in a full action run with sts-endpoint input', async () => {
vi.mocked(core.getInput).mockImplementation(
mocks.getInput({
...mocks.GH_OIDC_INPUTS,
'sts-endpoint': 'https://sts.custom.example.com',
}),
);
vi.mocked(core.getIDToken).mockResolvedValue('testoidctoken');
mockedSTSClient.on(GetCallerIdentityCommand).resolvesOnce({ ...mocks.outputs.GET_CALLER_IDENTITY });
mockedSTSClient.on(AssumeRoleWithWebIdentityCommand).resolvesOnce(mocks.outputs.STS_CREDENTIALS);
process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN = 'fake-token';
await run();
expect(core.setFailed).not.toHaveBeenCalled();
expect(core.info).toHaveBeenCalledWith('Authenticated as assumedRoleId AROAFAKEASSUMEDROLEID');
});
});
describe('HTTP Proxy Configuration', {}, () => {
beforeEach(() => {
vi.mocked(core.getInput).mockImplementation(mocks.getInput(mocks.GH_OIDC_INPUTS));
+127 -113
View File
@@ -95,7 +95,10 @@ describe('Profile Manager', {}, () => {
});
it('round-trips through parseIni', {}, () => {
const data = { dev: { aws_access_key_id: 'AKIA', aws_secret_access_key: 'secret' }, 'profile prod': { region: 'us-west-2' } };
const data = {
dev: { aws_access_key_id: 'AKIA', aws_secret_access_key: 'secret' },
'profile prod': { region: 'us-west-2' },
};
const roundTripped = parseIni(stringifyIni(data));
expect(roundTripped).toEqual(data);
});
@@ -196,10 +199,15 @@ describe('Profile Manager', {}, () => {
const filePath = '/home/runner/.aws/credentials';
fs.mkdirSync('/home/runner/.aws', { recursive: true });
mergeProfileSection(filePath, 'dev', {
aws_access_key_id: 'AKIAIOSFODNN7EXAMPLE',
aws_secret_access_key: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',
}, false);
mergeProfileSection(
filePath,
'dev',
{
aws_access_key_id: 'AKIAIOSFODNN7EXAMPLE',
aws_secret_access_key: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',
},
false,
);
const content = fs.readFileSync(filePath, 'utf-8');
const parsed = parseIni(content);
@@ -214,16 +222,26 @@ describe('Profile Manager', {}, () => {
fs.mkdirSync('/home/runner/.aws', { recursive: true });
// Create initial profile
mergeProfileSection(filePath, 'dev', {
aws_access_key_id: 'AKIAIOSFODNN7EXAMPLE',
aws_secret_access_key: 'devSecretKey',
}, false);
mergeProfileSection(
filePath,
'dev',
{
aws_access_key_id: 'AKIAIOSFODNN7EXAMPLE',
aws_secret_access_key: 'devSecretKey',
},
false,
);
// Add second profile
mergeProfileSection(filePath, 'prod', {
aws_access_key_id: 'AKIAPRODEXAMPLE',
aws_secret_access_key: 'prodSecretKey',
}, false);
mergeProfileSection(
filePath,
'prod',
{
aws_access_key_id: 'AKIAPRODEXAMPLE',
aws_secret_access_key: 'prodSecretKey',
},
false,
);
const content = fs.readFileSync(filePath, 'utf-8');
const parsed = parseIni(content);
@@ -239,18 +257,28 @@ describe('Profile Manager', {}, () => {
fs.mkdirSync('/home/runner/.aws', { recursive: true });
// Create initial profile
mergeProfileSection(filePath, 'dev', {
aws_access_key_id: 'OLD_KEY',
aws_secret_access_key: 'oldSecretKey',
aws_session_token: 'oldSessionToken'
}, false);
mergeProfileSection(
filePath,
'dev',
{
aws_access_key_id: 'OLD_KEY',
aws_secret_access_key: 'oldSecretKey',
aws_session_token: 'oldSessionToken',
},
false,
);
// Overwrite with new credentials
mergeProfileSection(filePath, 'dev', {
aws_access_key_id: 'NEW_KEY',
aws_secret_access_key: 'newSecretKey',
aws_session_token: 'newSessionToken',
}, true);
mergeProfileSection(
filePath,
'dev',
{
aws_access_key_id: 'NEW_KEY',
aws_secret_access_key: 'newSecretKey',
aws_session_token: 'newSessionToken',
},
true,
);
const content = fs.readFileSync(filePath, 'utf-8');
const parsed = parseIni(content);
@@ -265,17 +293,27 @@ describe('Profile Manager', {}, () => {
fs.mkdirSync('/home/runner/.aws', { recursive: true });
// Create profile with session token
mergeProfileSection(filePath, 'dev', {
aws_access_key_id: 'AKIA',
aws_secret_access_key: 'secret',
aws_session_token: 'old-token',
}, false);
mergeProfileSection(
filePath,
'dev',
{
aws_access_key_id: 'AKIA',
aws_secret_access_key: 'secret',
aws_session_token: 'old-token',
},
false,
);
// Overwrite without session token
mergeProfileSection(filePath, 'dev', {
aws_access_key_id: 'AKIA2',
aws_secret_access_key: 'secret2',
}, true);
mergeProfileSection(
filePath,
'dev',
{
aws_access_key_id: 'AKIA2',
aws_secret_access_key: 'secret2',
},
true,
);
const content = fs.readFileSync(filePath, 'utf-8');
const parsed = parseIni(content);
@@ -290,10 +328,15 @@ describe('Profile Manager', {}, () => {
fs.mkdirSync('/home/runner/.aws', { recursive: true });
fs.writeFileSync(filePath, '', { mode: 0o600 });
mergeProfileSection(filePath, 'dev', {
aws_access_key_id: 'AKIA',
aws_secret_access_key: 'secret',
}, false);
mergeProfileSection(
filePath,
'dev',
{
aws_access_key_id: 'AKIA',
aws_secret_access_key: 'secret',
},
false,
);
const content = fs.readFileSync(filePath, 'utf-8');
const parsed = parseIni(content);
@@ -307,17 +350,31 @@ describe('Profile Manager', {}, () => {
fs.mkdirSync('/home/runner/.aws', { recursive: true });
// Create initial profile
mergeProfileSection(filePath, 'dev', {
aws_access_key_id: 'OLD_KEY',
aws_secret_access_key: 'oldSecretKey',
}, false);
mergeProfileSection(
filePath,
'dev',
{
aws_access_key_id: 'OLD_KEY',
aws_secret_access_key: 'oldSecretKey',
},
false,
);
// Overwrite with new credentials
expect(() => mergeProfileSection(filePath, 'dev', {
aws_access_key_id: 'NEW_KEY',
aws_secret_access_key: 'newSecretKey',
aws_session_token: 'sessionToken',
}, false)).toThrow(`Profile with name "dev" already exists. Please use the overwrite-aws-profile input if you want to overwrite existing profiles.`);
expect(() =>
mergeProfileSection(
filePath,
'dev',
{
aws_access_key_id: 'NEW_KEY',
aws_secret_access_key: 'newSecretKey',
aws_session_token: 'sessionToken',
},
false,
),
).toThrow(
`Profile with name "dev" already exists. Please use the overwrite-aws-profile input if you want to overwrite existing profiles.`,
);
const content = fs.readFileSync(filePath, 'utf-8');
const parsed = parseIni(content);
@@ -468,7 +525,7 @@ describe('Profile Manager', {}, () => {
SecretAccessKey: 'secret',
},
'us-east-1',
false
false,
),
).toThrow('whitespace');
});
@@ -486,7 +543,7 @@ describe('Profile Manager', {}, () => {
SecretAccessKey: 'secret',
},
'us-east-1',
false
false,
);
expect(fs.existsSync('/custom/credentials')).toBe(true);
@@ -501,7 +558,7 @@ describe('Profile Manager', {}, () => {
SecretAccessKey: 'secret',
},
'us-east-1',
false
false,
);
expect(core.info).toHaveBeenCalledWith('Writing credentials to profile: dev');
@@ -517,12 +574,7 @@ describe('Profile Manager', {}, () => {
'[personal]\naws_access_key_id=AKIAPERSONAL\naws_secret_access_key=personalSecret\naws_session_token=personalToken\n',
);
writeProfileFiles(
'dev',
{ AccessKeyId: 'AKIADEV', SecretAccessKey: 'devSecret' },
'us-east-1',
false,
);
writeProfileFiles('dev', { AccessKeyId: 'AKIADEV', SecretAccessKey: 'devSecret' }, 'us-east-1', false);
const content = fs.readFileSync(credsPath, 'utf-8');
const parsed = parseIni(content);
@@ -540,17 +592,9 @@ describe('Profile Manager', {}, () => {
it('preserves pre-existing config with extra keys', {}, () => {
const configPath = getProfileFilePaths().config;
fs.mkdirSync(require('node:path').dirname(configPath), { recursive: true });
fs.writeFileSync(
configPath,
'[profile personal]\nregion=eu-west-1\noutput=json\ncli_pager=\n',
);
fs.writeFileSync(configPath, '[profile personal]\nregion=eu-west-1\noutput=json\ncli_pager=\n');
writeProfileFiles(
'dev',
{ AccessKeyId: 'AKIA', SecretAccessKey: 'secret' },
'us-east-1',
false
);
writeProfileFiles('dev', { AccessKeyId: 'AKIA', SecretAccessKey: 'secret' }, 'us-east-1', false);
const content = fs.readFileSync(configPath, 'utf-8');
const parsed = parseIni(content);
@@ -566,17 +610,9 @@ describe('Profile Manager', {}, () => {
it('preserves pre-existing default profile when writing a named profile', {}, () => {
const credsPath = getProfileFilePaths().credentials;
fs.mkdirSync(require('node:path').dirname(credsPath), { recursive: true });
fs.writeFileSync(
credsPath,
'[default]\naws_access_key_id=AKIADEFAULT\naws_secret_access_key=defaultSecret\n',
);
fs.writeFileSync(credsPath, '[default]\naws_access_key_id=AKIADEFAULT\naws_secret_access_key=defaultSecret\n');
writeProfileFiles(
'dev',
{ AccessKeyId: 'AKIADEV', SecretAccessKey: 'devSecret' },
'us-west-2',
false
);
writeProfileFiles('dev', { AccessKeyId: 'AKIADEV', SecretAccessKey: 'devSecret' }, 'us-west-2', false);
const content = fs.readFileSync(credsPath, 'utf-8');
const parsed = parseIni(content);
@@ -596,12 +632,7 @@ describe('Profile Manager', {}, () => {
'# My important comment\n[personal]\naws_access_key_id=AKIA\naws_secret_access_key=secret\n',
);
writeProfileFiles(
'dev',
{ AccessKeyId: 'AKIADEV', SecretAccessKey: 'devSecret' },
'us-east-1',
false
);
writeProfileFiles('dev', { AccessKeyId: 'AKIADEV', SecretAccessKey: 'devSecret' }, 'us-east-1', false);
const content = fs.readFileSync(credsPath, 'utf-8') as string;
@@ -635,12 +666,7 @@ describe('Profile Manager', {}, () => {
fs.mkdirSync('/custom-creds', { recursive: true });
writeProfileFiles(
'dev',
{ AccessKeyId: 'AKIA', SecretAccessKey: 'secret' },
'us-east-1',
false
);
writeProfileFiles('dev', { AccessKeyId: 'AKIA', SecretAccessKey: 'secret' }, 'us-east-1', false);
expect(fs.existsSync('/custom-creds/credentials')).toBe(true);
// Config file should be at the default path (under homedir)
@@ -657,7 +683,7 @@ describe('Profile Manager', {}, () => {
SessionToken: 'FwoGZXIvYXdzEBYaDEXAMPLE',
},
'us-east-1',
false
false,
);
const credsPath = getProfileFilePaths().credentials;
@@ -672,28 +698,20 @@ describe('Profile Manager', {}, () => {
// - LF line endings, trailing newline
expect(credContent).toBe(
'[dev]\n' +
'aws_access_key_id = AKIAIOSFODNN7EXAMPLE\n' +
'aws_secret_access_key = wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY\n' +
'aws_session_token = FwoGZXIvYXdzEBYaDEXAMPLE\n',
);
expect(configContent).toBe(
'[profile dev]\n' +
'region = us-east-1\n',
'aws_access_key_id = AKIAIOSFODNN7EXAMPLE\n' +
'aws_secret_access_key = wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY\n' +
'aws_session_token = FwoGZXIvYXdzEBYaDEXAMPLE\n',
);
expect(configContent).toBe('[profile dev]\n' + 'region = us-east-1\n');
});
it('golden file for multi-profile output', {}, () => {
writeProfileFiles(
'dev',
{ AccessKeyId: 'AKIADEV', SecretAccessKey: 'devSecret' },
'us-east-1',
false
);
writeProfileFiles('dev', { AccessKeyId: 'AKIADEV', SecretAccessKey: 'devSecret' }, 'us-east-1', false);
writeProfileFiles(
'prod',
{ AccessKeyId: 'AKIAPROD', SecretAccessKey: 'prodSecret', SessionToken: 'prodToken' },
'us-west-2',
false
false,
);
const credsPath = getProfileFilePaths().credentials;
@@ -704,20 +722,16 @@ describe('Profile Manager', {}, () => {
expect(credContent).toBe(
'[dev]\n' +
'aws_access_key_id = AKIADEV\n' +
'aws_secret_access_key = devSecret\n' +
'\n' +
'[prod]\n' +
'aws_access_key_id = AKIAPROD\n' +
'aws_secret_access_key = prodSecret\n' +
'aws_session_token = prodToken\n',
'aws_access_key_id = AKIADEV\n' +
'aws_secret_access_key = devSecret\n' +
'\n' +
'[prod]\n' +
'aws_access_key_id = AKIAPROD\n' +
'aws_secret_access_key = prodSecret\n' +
'aws_session_token = prodToken\n',
);
expect(configContent).toBe(
'[profile dev]\n' +
'region = us-east-1\n' +
'\n' +
'[profile prod]\n' +
'region = us-west-2\n',
'[profile dev]\n' + 'region = us-east-1\n' + '\n' + '[profile prod]\n' + 'region = us-west-2\n',
);
});
});