New package for AWS integration node library
Signed-off-by: Clare Liguori <liguori@amazon.com>
This commit is contained in:
@@ -0,0 +1,430 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { DefaultAwsCredentialsProvider } from './DefaultAwsCredentialsProvider';
|
||||
import { mockClient, AwsClientStub } from 'aws-sdk-client-mock';
|
||||
import 'aws-sdk-client-mock-jest';
|
||||
import {
|
||||
STSClient,
|
||||
GetCallerIdentityCommand,
|
||||
AssumeRoleCommand,
|
||||
} from '@aws-sdk/client-sts';
|
||||
import { Config, ConfigReader } from '@backstage/config';
|
||||
import { promises } from 'fs';
|
||||
|
||||
const env = process.env;
|
||||
let stsMock: AwsClientStub<STSClient>;
|
||||
let config: Config;
|
||||
|
||||
jest.mock('fs', () => ({ promises: { readFile: jest.fn() } }));
|
||||
|
||||
describe('DefaultAwsCredentialsProvider', () => {
|
||||
beforeEach(() => {
|
||||
process.env = { ...env };
|
||||
jest.resetAllMocks();
|
||||
|
||||
stsMock = mockClient(STSClient);
|
||||
|
||||
config = new ConfigReader({
|
||||
aws: {
|
||||
accounts: [
|
||||
{
|
||||
accountId: '111111111111',
|
||||
roleName: 'hello',
|
||||
externalId: 'world',
|
||||
},
|
||||
{
|
||||
accountId: '222222222222',
|
||||
roleName: 'hi',
|
||||
partition: 'aws-other',
|
||||
region: 'not-us-east-1',
|
||||
accessKeyId: 'ABC',
|
||||
secretAccessKey: 'EDF',
|
||||
},
|
||||
{
|
||||
accountId: '333333333333',
|
||||
accessKeyId: 'my-access-key',
|
||||
secretAccessKey: 'my-secret-access-key',
|
||||
},
|
||||
{
|
||||
accountId: '444444444444',
|
||||
},
|
||||
{
|
||||
accountId: '555555555555',
|
||||
profile: 'my-profile',
|
||||
},
|
||||
],
|
||||
accountDefaults: {
|
||||
roleName: 'backstage-role',
|
||||
externalId: 'my-id',
|
||||
},
|
||||
mainAccount: {
|
||||
accessKeyId: 'GHI',
|
||||
secretAccessKey: 'JKL',
|
||||
region: 'ap-northeast-1',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
stsMock.on(GetCallerIdentityCommand).resolvesOnce({
|
||||
Account: '123456789012',
|
||||
});
|
||||
|
||||
stsMock
|
||||
.on(AssumeRoleCommand, {
|
||||
RoleArn: 'arn:aws:iam::111111111111:role/hello',
|
||||
RoleSessionName: 'backstage',
|
||||
ExternalId: 'world',
|
||||
})
|
||||
.resolves({
|
||||
Credentials: {
|
||||
AccessKeyId: 'ACCESS_KEY_ID_1',
|
||||
SecretAccessKey: 'SECRET_ACCESS_KEY_1',
|
||||
SessionToken: 'SESSION_TOKEN_1',
|
||||
Expiration: new Date('2022-01-01'),
|
||||
},
|
||||
});
|
||||
|
||||
stsMock
|
||||
.on(AssumeRoleCommand, {
|
||||
RoleArn: 'arn:aws-other:iam::222222222222:role/hi',
|
||||
RoleSessionName: 'backstage',
|
||||
})
|
||||
.resolves({
|
||||
Credentials: {
|
||||
AccessKeyId: 'ACCESS_KEY_ID_2',
|
||||
SecretAccessKey: 'SECRET_ACCESS_KEY_2',
|
||||
SessionToken: 'SESSION_TOKEN_2',
|
||||
Expiration: new Date('2022-01-02'),
|
||||
},
|
||||
});
|
||||
|
||||
stsMock
|
||||
.on(AssumeRoleCommand, {
|
||||
RoleArn: 'arn:aws:iam::999999999999:role/backstage-role',
|
||||
RoleSessionName: 'backstage',
|
||||
ExternalId: 'my-id',
|
||||
})
|
||||
.resolves({
|
||||
Credentials: {
|
||||
AccessKeyId: 'ACCESS_KEY_ID_9',
|
||||
SecretAccessKey: 'SECRET_ACCESS_KEY_9',
|
||||
SessionToken: 'SESSION_TOKEN_9',
|
||||
Expiration: new Date('2022-01-09'),
|
||||
},
|
||||
});
|
||||
|
||||
process.env.AWS_ACCESS_KEY_ID = 'ACCESS_KEY_ID_10';
|
||||
process.env.AWS_SECRET_ACCESS_KEY = 'SECRET_ACCESS_KEY_10';
|
||||
process.env.AWS_SESSION_TOKEN = 'SESSION_TOKEN_10';
|
||||
process.env.AWS_CREDENTIAL_EXPIRATION = new Date(
|
||||
'2022-01-10',
|
||||
).toISOString();
|
||||
|
||||
const mockProfile = `[my-profile]
|
||||
aws_access_key_id=ACCESS_KEY_ID_9
|
||||
aws_secret_access_key=SECRET_ACCESS_KEY_9
|
||||
`;
|
||||
(promises.readFile as jest.Mock).mockResolvedValue(mockProfile);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.env = env;
|
||||
});
|
||||
|
||||
describe('#getCredentials', () => {
|
||||
it('retrieves assume-role creds for the given account ID and caches the provider', async () => {
|
||||
const provider = DefaultAwsCredentialsProvider.fromConfig(config);
|
||||
const awsCredentials = await provider.getCredentials({
|
||||
accountId: '111111111111',
|
||||
});
|
||||
|
||||
expect(awsCredentials.accountId).toEqual('111111111111');
|
||||
|
||||
const creds = await awsCredentials.provider();
|
||||
expect(creds).toEqual({
|
||||
accessKeyId: 'ACCESS_KEY_ID_1',
|
||||
secretAccessKey: 'SECRET_ACCESS_KEY_1',
|
||||
sessionToken: 'SESSION_TOKEN_1',
|
||||
expiration: new Date('2022-01-01'),
|
||||
});
|
||||
|
||||
const awsCredentials2 = await provider.getCredentials({
|
||||
accountId: '111111111111',
|
||||
});
|
||||
|
||||
expect(awsCredentials).toBe(awsCredentials2);
|
||||
expect(stsMock).toHaveReceivedCommandTimes(AssumeRoleCommand, 1);
|
||||
});
|
||||
|
||||
it('retrieves assume-role creds in another partition for the given account ID', async () => {
|
||||
const provider = DefaultAwsCredentialsProvider.fromConfig(config);
|
||||
const awsCredentials = await provider.getCredentials({
|
||||
accountId: '222222222222',
|
||||
});
|
||||
|
||||
expect(awsCredentials.accountId).toEqual('222222222222');
|
||||
|
||||
const creds = await awsCredentials.provider();
|
||||
expect(creds).toEqual({
|
||||
accessKeyId: 'ACCESS_KEY_ID_2',
|
||||
secretAccessKey: 'SECRET_ACCESS_KEY_2',
|
||||
sessionToken: 'SESSION_TOKEN_2',
|
||||
expiration: new Date('2022-01-02'),
|
||||
});
|
||||
});
|
||||
|
||||
it('retrieves assume-role creds for an account using the account defaults', async () => {
|
||||
const provider = DefaultAwsCredentialsProvider.fromConfig(config);
|
||||
const awsCredentials = await provider.getCredentials({
|
||||
accountId: '999999999999',
|
||||
});
|
||||
|
||||
expect(awsCredentials.accountId).toEqual('999999999999');
|
||||
|
||||
const creds = await awsCredentials.provider();
|
||||
expect(creds).toEqual({
|
||||
accessKeyId: 'ACCESS_KEY_ID_9',
|
||||
secretAccessKey: 'SECRET_ACCESS_KEY_9',
|
||||
sessionToken: 'SESSION_TOKEN_9',
|
||||
expiration: new Date('2022-01-09'),
|
||||
});
|
||||
});
|
||||
|
||||
it('retrieves static creds for the given account ID', async () => {
|
||||
const provider = DefaultAwsCredentialsProvider.fromConfig(config);
|
||||
const awsCredentials = await provider.getCredentials({
|
||||
accountId: '333333333333',
|
||||
});
|
||||
|
||||
expect(awsCredentials.accountId).toEqual('333333333333');
|
||||
|
||||
const creds = await awsCredentials.provider();
|
||||
expect(creds).toEqual({
|
||||
accessKeyId: 'my-access-key',
|
||||
secretAccessKey: 'my-secret-access-key',
|
||||
});
|
||||
});
|
||||
|
||||
it('retrieves static creds from the main account', async () => {
|
||||
const minConfig = new ConfigReader({
|
||||
aws: {
|
||||
mainAccount: {
|
||||
accessKeyId: 'GHI',
|
||||
secretAccessKey: 'JKL',
|
||||
},
|
||||
},
|
||||
});
|
||||
const provider = DefaultAwsCredentialsProvider.fromConfig(minConfig);
|
||||
const awsCredentials = await provider.getCredentials({
|
||||
accountId: '123456789012',
|
||||
});
|
||||
|
||||
expect(awsCredentials.accountId).toEqual('123456789012');
|
||||
|
||||
const creds = await awsCredentials.provider();
|
||||
expect(creds).toEqual({
|
||||
accessKeyId: 'GHI',
|
||||
secretAccessKey: 'JKL',
|
||||
});
|
||||
});
|
||||
|
||||
it('only queries the main account ID once from STS', async () => {
|
||||
const minConfig = new ConfigReader({
|
||||
aws: {
|
||||
mainAccount: {
|
||||
accessKeyId: 'GHI',
|
||||
secretAccessKey: 'JKL',
|
||||
},
|
||||
},
|
||||
});
|
||||
const provider = DefaultAwsCredentialsProvider.fromConfig(minConfig);
|
||||
const awsCredentials1 = await provider.getCredentials({});
|
||||
const awsCredentials2 = await provider.getCredentials({});
|
||||
|
||||
expect(awsCredentials1).toBe(awsCredentials2);
|
||||
expect(stsMock).toHaveReceivedCommandTimes(GetCallerIdentityCommand, 1);
|
||||
});
|
||||
|
||||
it('retrieves the ini provider chain for the given account ID', async () => {
|
||||
const provider = DefaultAwsCredentialsProvider.fromConfig(config);
|
||||
const awsCredentials = await provider.getCredentials({
|
||||
accountId: '555555555555',
|
||||
});
|
||||
|
||||
expect(awsCredentials.accountId).toEqual('555555555555');
|
||||
|
||||
const creds = await awsCredentials.provider();
|
||||
expect(creds).toEqual({
|
||||
accessKeyId: 'ACCESS_KEY_ID_9',
|
||||
secretAccessKey: 'SECRET_ACCESS_KEY_9',
|
||||
});
|
||||
});
|
||||
|
||||
it('retrieves the default cred provider chain for the given account ID', async () => {
|
||||
const provider = DefaultAwsCredentialsProvider.fromConfig(config);
|
||||
const awsCredentials = await provider.getCredentials({
|
||||
accountId: '444444444444',
|
||||
});
|
||||
|
||||
expect(awsCredentials.accountId).toEqual('444444444444');
|
||||
|
||||
const creds = await awsCredentials.provider();
|
||||
expect(creds).toEqual({
|
||||
accessKeyId: 'ACCESS_KEY_ID_10',
|
||||
secretAccessKey: 'SECRET_ACCESS_KEY_10',
|
||||
sessionToken: 'SESSION_TOKEN_10',
|
||||
expiration: new Date('2022-01-10'),
|
||||
});
|
||||
});
|
||||
|
||||
it('retrieves ini provider chain from the main account', async () => {
|
||||
const minConfig = new ConfigReader({
|
||||
aws: {
|
||||
mainAccount: {
|
||||
profile: 'my-profile',
|
||||
},
|
||||
},
|
||||
});
|
||||
const provider = DefaultAwsCredentialsProvider.fromConfig(minConfig);
|
||||
const awsCredentials = await provider.getCredentials({
|
||||
accountId: '123456789012',
|
||||
});
|
||||
|
||||
expect(awsCredentials.accountId).toEqual('123456789012');
|
||||
|
||||
const creds = await awsCredentials.provider();
|
||||
expect(creds).toEqual({
|
||||
accessKeyId: 'ACCESS_KEY_ID_9',
|
||||
secretAccessKey: 'SECRET_ACCESS_KEY_9',
|
||||
});
|
||||
});
|
||||
|
||||
it('retrieves default cred provider chain from the main account', async () => {
|
||||
const minConfig = new ConfigReader({
|
||||
aws: {},
|
||||
});
|
||||
const provider = DefaultAwsCredentialsProvider.fromConfig(minConfig);
|
||||
const awsCredentials = await provider.getCredentials({
|
||||
accountId: '123456789012',
|
||||
});
|
||||
|
||||
expect(awsCredentials.accountId).toEqual('123456789012');
|
||||
|
||||
const creds = await awsCredentials.provider();
|
||||
expect(creds).toEqual({
|
||||
accessKeyId: 'ACCESS_KEY_ID_10',
|
||||
secretAccessKey: 'SECRET_ACCESS_KEY_10',
|
||||
sessionToken: 'SESSION_TOKEN_10',
|
||||
expiration: new Date('2022-01-10'),
|
||||
});
|
||||
});
|
||||
|
||||
it('retrieves default cred provider chain from the main account when there is no AWS integration config', async () => {
|
||||
const minConfig = new ConfigReader({});
|
||||
const provider = DefaultAwsCredentialsProvider.fromConfig(minConfig);
|
||||
const awsCredentials = await provider.getCredentials({
|
||||
accountId: '123456789012',
|
||||
});
|
||||
|
||||
expect(awsCredentials.accountId).toEqual('123456789012');
|
||||
|
||||
const creds = await awsCredentials.provider();
|
||||
expect(creds).toEqual({
|
||||
accessKeyId: 'ACCESS_KEY_ID_10',
|
||||
secretAccessKey: 'SECRET_ACCESS_KEY_10',
|
||||
sessionToken: 'SESSION_TOKEN_10',
|
||||
expiration: new Date('2022-01-10'),
|
||||
});
|
||||
});
|
||||
|
||||
it('extracts the account ID from an ARN', async () => {
|
||||
const provider = DefaultAwsCredentialsProvider.fromConfig(config);
|
||||
const awsCredentials = await provider.getCredentials({
|
||||
arn: 'arn:aws:ecs:region:111111111111:service/cluster-name/service-name',
|
||||
});
|
||||
|
||||
expect(awsCredentials.accountId).toEqual('111111111111');
|
||||
|
||||
const creds = await awsCredentials.provider();
|
||||
expect(creds).toEqual({
|
||||
accessKeyId: 'ACCESS_KEY_ID_1',
|
||||
secretAccessKey: 'SECRET_ACCESS_KEY_1',
|
||||
sessionToken: 'SESSION_TOKEN_1',
|
||||
expiration: new Date('2022-01-01'),
|
||||
});
|
||||
});
|
||||
|
||||
it('falls back to main account credentials when account ID cannot be extracted from the ARN', async () => {
|
||||
const provider = DefaultAwsCredentialsProvider.fromConfig(config);
|
||||
const awsCredentials = await provider.getCredentials({
|
||||
arn: 'arn:aws:s3:::bucket_name',
|
||||
});
|
||||
|
||||
expect(awsCredentials.accountId).toEqual('123456789012');
|
||||
|
||||
const creds = await awsCredentials.provider();
|
||||
expect(creds).toEqual({
|
||||
accessKeyId: 'GHI',
|
||||
secretAccessKey: 'JKL',
|
||||
});
|
||||
});
|
||||
|
||||
it('falls back to main account credentials when neither account ID nor ARN are provided', async () => {
|
||||
const provider = DefaultAwsCredentialsProvider.fromConfig(config);
|
||||
const awsCredentials = await provider.getCredentials({});
|
||||
|
||||
expect(awsCredentials.accountId).toEqual('123456789012');
|
||||
|
||||
const creds = await awsCredentials.provider();
|
||||
expect(creds).toEqual({
|
||||
accessKeyId: 'GHI',
|
||||
secretAccessKey: 'JKL',
|
||||
});
|
||||
});
|
||||
|
||||
it('falls back to main account credentials when no options are provided', async () => {
|
||||
const provider = DefaultAwsCredentialsProvider.fromConfig(config);
|
||||
const awsCredentials = await provider.getCredentials();
|
||||
|
||||
expect(awsCredentials.accountId).toEqual('123456789012');
|
||||
|
||||
const creds = await awsCredentials.provider();
|
||||
expect(creds).toEqual({
|
||||
accessKeyId: 'GHI',
|
||||
secretAccessKey: 'JKL',
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects account that is not configured, with no account defaults', async () => {
|
||||
const minConfig = new ConfigReader({
|
||||
aws: {},
|
||||
});
|
||||
const provider = DefaultAwsCredentialsProvider.fromConfig(minConfig);
|
||||
await expect(
|
||||
provider.getCredentials({ accountId: '111222333444' }),
|
||||
).rejects.toThrow(/no AWS integration that matches 111222333444/);
|
||||
});
|
||||
|
||||
it('rejects main account that has invalid credentials', async () => {
|
||||
stsMock.on(GetCallerIdentityCommand).rejects('No credentials found');
|
||||
const provider = DefaultAwsCredentialsProvider.fromConfig(config);
|
||||
await expect(provider.getCredentials({})).rejects.toThrow(
|
||||
/No credentials found/,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,274 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
readAwsIntegrationConfig,
|
||||
AwsIntegrationAccountConfig,
|
||||
AwsIntegrationDefaultAccountConfig,
|
||||
AwsIntegrationMainAccountConfig,
|
||||
} from './config';
|
||||
import {
|
||||
AwsCredentials,
|
||||
AwsCredentialsProvider,
|
||||
AwsCredentialsProviderOptions,
|
||||
} from './types';
|
||||
import { GetCallerIdentityCommand, STSClient } from '@aws-sdk/client-sts';
|
||||
import {
|
||||
fromIni,
|
||||
fromNodeProviderChain,
|
||||
fromTemporaryCredentials,
|
||||
} from '@aws-sdk/credential-providers';
|
||||
import { AwsCredentialIdentityProvider } from '@aws-sdk/types';
|
||||
import { parse } from '@aws-sdk/util-arn-parser';
|
||||
import { Config } from '@backstage/config';
|
||||
|
||||
/**
|
||||
* Retrieves the account ID for the given credentials provider from STS.
|
||||
*/
|
||||
async function fillInAccountId(creds: AwsCredentials) {
|
||||
if (creds.accountId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const client = new STSClient({
|
||||
region: creds.stsRegion,
|
||||
customUserAgent: 'backstage-aws-credentials-provider',
|
||||
credentialDefaultProvider: () => creds.provider,
|
||||
});
|
||||
const resp = await client.send(new GetCallerIdentityCommand({}));
|
||||
creds.accountId = resp.Account!;
|
||||
}
|
||||
|
||||
function getStaticCredentials(
|
||||
accessKeyId: string,
|
||||
secretAccessKey: string,
|
||||
): AwsCredentialIdentityProvider {
|
||||
return async () => {
|
||||
return Promise.resolve({
|
||||
accessKeyId: accessKeyId,
|
||||
secretAccessKey: secretAccessKey,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function getProfileCredentials(
|
||||
profile: string,
|
||||
region?: string,
|
||||
): AwsCredentialIdentityProvider {
|
||||
return fromIni({
|
||||
profile,
|
||||
clientConfig: {
|
||||
region,
|
||||
customUserAgent: 'backstage-aws-credentials-provider',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function getDefaultCredentialsChain(): AwsCredentialIdentityProvider {
|
||||
return fromNodeProviderChain();
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs the credential provider needed by the AWS SDK from the given account config
|
||||
*
|
||||
* Order of precedence:
|
||||
* 1. Assume role with static creds
|
||||
* 2. Assume role with main account creds
|
||||
* 3. Static creds
|
||||
* 4. Profile creds
|
||||
* 5. Default AWS SDK creds chain
|
||||
*/
|
||||
function getAccountCredentialsProvider(
|
||||
config: AwsIntegrationAccountConfig,
|
||||
mainAccountCreds: AwsCredentialIdentityProvider,
|
||||
): AwsCredentialIdentityProvider {
|
||||
if (config.roleName) {
|
||||
const region = config.region ?? 'us-east-1';
|
||||
const partition = config.partition ?? 'aws';
|
||||
|
||||
return fromTemporaryCredentials({
|
||||
masterCredentials: config.accessKeyId
|
||||
? getStaticCredentials(config.accessKeyId!, config.secretAccessKey!)
|
||||
: mainAccountCreds,
|
||||
params: {
|
||||
RoleArn: `arn:${partition}:iam::${config.accountId}:role/${config.roleName}`,
|
||||
RoleSessionName: 'backstage',
|
||||
ExternalId: config.externalId,
|
||||
},
|
||||
clientConfig: {
|
||||
region,
|
||||
customUserAgent: 'backstage-aws-credentials-provider',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (config.accessKeyId) {
|
||||
return getStaticCredentials(config.accessKeyId!, config.secretAccessKey!);
|
||||
}
|
||||
|
||||
if (config.profile) {
|
||||
return getProfileCredentials(config.profile!, config.region);
|
||||
}
|
||||
|
||||
return getDefaultCredentialsChain();
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs the credential provider needed by the AWS SDK for the main account
|
||||
*
|
||||
* Order of precedence:
|
||||
* 1. Static creds
|
||||
* 2. Profile creds
|
||||
* 3. Default AWS SDK creds chain
|
||||
*/
|
||||
function getMainAccountCredentialsProvider(
|
||||
config: AwsIntegrationMainAccountConfig,
|
||||
): AwsCredentialIdentityProvider {
|
||||
if (config.accessKeyId) {
|
||||
return getStaticCredentials(config.accessKeyId!, config.secretAccessKey!);
|
||||
}
|
||||
|
||||
if (config.profile) {
|
||||
return getProfileCredentials(config.profile!, config.region);
|
||||
}
|
||||
|
||||
return getDefaultCredentialsChain();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the creation and caching of credential providers for AWS accounts.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export class DefaultAwsCredentialsProvider implements AwsCredentialsProvider {
|
||||
static fromConfig(config: Config): DefaultAwsCredentialsProvider {
|
||||
const awsConfig = config.has('aws')
|
||||
? readAwsIntegrationConfig(config.getConfig('aws'))
|
||||
: {
|
||||
accounts: [],
|
||||
mainAccount: {},
|
||||
accountDefaults: {},
|
||||
};
|
||||
|
||||
const mainAccountProvider = getMainAccountCredentialsProvider(
|
||||
awsConfig.mainAccount,
|
||||
);
|
||||
const mainAccountCreds: AwsCredentials = {
|
||||
provider: mainAccountProvider,
|
||||
};
|
||||
|
||||
const accountCreds = new Map<string, AwsCredentials>();
|
||||
for (const accountConfig of awsConfig.accounts) {
|
||||
const provider = getAccountCredentialsProvider(
|
||||
accountConfig,
|
||||
mainAccountCreds.provider,
|
||||
);
|
||||
accountCreds.set(accountConfig.accountId, {
|
||||
accountId: accountConfig.accountId,
|
||||
stsRegion: accountConfig.region,
|
||||
provider,
|
||||
});
|
||||
}
|
||||
|
||||
return new DefaultAwsCredentialsProvider(
|
||||
accountCreds,
|
||||
awsConfig.accountDefaults,
|
||||
mainAccountCreds,
|
||||
);
|
||||
}
|
||||
|
||||
private constructor(
|
||||
private readonly accountCredentials: Map<string, AwsCredentials>,
|
||||
private readonly accountDefaults: AwsIntegrationDefaultAccountConfig,
|
||||
private readonly mainAccountCredentials: AwsCredentials,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Returns {@link AwsCredentials} for a given AWS account.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const { provider } = await getCredentials({
|
||||
* accountId: '0123456789012',
|
||||
* })
|
||||
*
|
||||
* const { provider } = await getCredentials({
|
||||
* arn: 'arn:aws:ecs:us-west-2:123456789012:service/my-http-service'
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
* @param opts - the AWS account ID or AWS resource ARN
|
||||
* @returns A promise of {@link AwsCredentials}.
|
||||
*/
|
||||
async getCredentials(
|
||||
opts?: AwsCredentialsProviderOptions,
|
||||
): Promise<AwsCredentials> {
|
||||
// If no options provided, fall back to the main account
|
||||
if (!opts) {
|
||||
await fillInAccountId(this.mainAccountCredentials);
|
||||
return this.mainAccountCredentials;
|
||||
}
|
||||
|
||||
// Determine the account ID: either explicitly provided or extracted from the provided ARN
|
||||
let accountId = opts.accountId;
|
||||
if (opts.arn && !accountId) {
|
||||
const arnComponents = parse(opts.arn);
|
||||
accountId = arnComponents.accountId;
|
||||
}
|
||||
|
||||
// If the account ID was not provided (explicitly or in the ARN),
|
||||
// fall back to the main account
|
||||
if (!accountId) {
|
||||
await fillInAccountId(this.mainAccountCredentials);
|
||||
return this.mainAccountCredentials;
|
||||
}
|
||||
|
||||
// Return a cached provider if available
|
||||
if (this.accountCredentials.has(accountId)) {
|
||||
return this.accountCredentials.get(accountId)!;
|
||||
}
|
||||
|
||||
// First, fall back to using the account defaults
|
||||
if (this.accountDefaults.roleName) {
|
||||
const config: AwsIntegrationAccountConfig = {
|
||||
accountId,
|
||||
roleName: this.accountDefaults.roleName,
|
||||
partition: this.accountDefaults.partition,
|
||||
region: this.accountDefaults.region,
|
||||
externalId: this.accountDefaults.externalId,
|
||||
};
|
||||
const provider = getAccountCredentialsProvider(
|
||||
config,
|
||||
this.mainAccountCredentials.provider,
|
||||
);
|
||||
const creds: AwsCredentials = { accountId, provider };
|
||||
this.accountCredentials.set(accountId, creds);
|
||||
return creds;
|
||||
}
|
||||
|
||||
// Then, fall back to using the main account, but only
|
||||
// if the account requested matches the main account ID
|
||||
await fillInAccountId(this.mainAccountCredentials);
|
||||
if (accountId === this.mainAccountCredentials.accountId) {
|
||||
return this.mainAccountCredentials;
|
||||
}
|
||||
|
||||
// Otherwise, the account needs to be explicitly configured in Backstage
|
||||
throw new Error(
|
||||
`There is no AWS integration that matches ${accountId}. Please add a configuration for this AWS account.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,335 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Config, ConfigReader } from '@backstage/config';
|
||||
import { AwsIntegrationConfig, readAwsIntegrationConfig } from './config';
|
||||
|
||||
describe('readAwsIntegrationConfig', () => {
|
||||
function buildConfig(data: Partial<AwsIntegrationConfig>): Config {
|
||||
return new ConfigReader(data);
|
||||
}
|
||||
|
||||
it('reads all values', () => {
|
||||
const output = readAwsIntegrationConfig(
|
||||
buildConfig({
|
||||
accounts: [
|
||||
{
|
||||
accountId: '111111111111',
|
||||
accessKeyId: 'ABC',
|
||||
secretAccessKey: 'EDF',
|
||||
roleName: 'hello',
|
||||
partition: 'aws',
|
||||
region: 'us-east-1',
|
||||
externalId: 'world',
|
||||
},
|
||||
{
|
||||
accountId: '222222222222',
|
||||
accessKeyId: 'GHI',
|
||||
secretAccessKey: 'JKL',
|
||||
},
|
||||
{
|
||||
accountId: '333333333333',
|
||||
roleName: 'hi',
|
||||
partition: 'aws-other',
|
||||
region: 'not-us-east-1',
|
||||
externalId: 'there',
|
||||
},
|
||||
{
|
||||
accountId: '444444444444',
|
||||
profile: 'my-profile',
|
||||
},
|
||||
],
|
||||
accountDefaults: {
|
||||
roleName: 'backstage-role',
|
||||
partition: 'aws',
|
||||
region: 'us-east-1',
|
||||
externalId: 'my-id',
|
||||
},
|
||||
mainAccount: {
|
||||
accessKeyId: 'GHI',
|
||||
secretAccessKey: 'JKL',
|
||||
region: 'ap-northeast-1',
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(output).toEqual({
|
||||
accounts: [
|
||||
{
|
||||
accountId: '111111111111',
|
||||
accessKeyId: 'ABC',
|
||||
secretAccessKey: 'EDF',
|
||||
roleName: 'hello',
|
||||
partition: 'aws',
|
||||
region: 'us-east-1',
|
||||
externalId: 'world',
|
||||
},
|
||||
{
|
||||
accountId: '222222222222',
|
||||
accessKeyId: 'GHI',
|
||||
secretAccessKey: 'JKL',
|
||||
},
|
||||
{
|
||||
accountId: '333333333333',
|
||||
roleName: 'hi',
|
||||
partition: 'aws-other',
|
||||
region: 'not-us-east-1',
|
||||
externalId: 'there',
|
||||
},
|
||||
{
|
||||
accountId: '444444444444',
|
||||
profile: 'my-profile',
|
||||
},
|
||||
],
|
||||
accountDefaults: {
|
||||
roleName: 'backstage-role',
|
||||
partition: 'aws',
|
||||
region: 'us-east-1',
|
||||
externalId: 'my-id',
|
||||
},
|
||||
mainAccount: {
|
||||
accessKeyId: 'GHI',
|
||||
secretAccessKey: 'JKL',
|
||||
region: 'ap-northeast-1',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('reads profile for main account', () => {
|
||||
const output = readAwsIntegrationConfig(
|
||||
buildConfig({
|
||||
accounts: [
|
||||
{
|
||||
accountId: '111111111111',
|
||||
accessKeyId: 'ABC',
|
||||
secretAccessKey: 'EDF',
|
||||
roleName: 'hello',
|
||||
partition: 'aws',
|
||||
region: 'us-east-1',
|
||||
externalId: 'world',
|
||||
},
|
||||
],
|
||||
accountDefaults: {
|
||||
roleName: 'backstage-role',
|
||||
partition: 'aws',
|
||||
region: 'us-east-1',
|
||||
externalId: 'my-id',
|
||||
},
|
||||
mainAccount: {
|
||||
profile: 'my-profile',
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(output).toEqual({
|
||||
accounts: [
|
||||
{
|
||||
accountId: '111111111111',
|
||||
accessKeyId: 'ABC',
|
||||
secretAccessKey: 'EDF',
|
||||
roleName: 'hello',
|
||||
partition: 'aws',
|
||||
region: 'us-east-1',
|
||||
externalId: 'world',
|
||||
},
|
||||
],
|
||||
accountDefaults: {
|
||||
roleName: 'backstage-role',
|
||||
partition: 'aws',
|
||||
region: 'us-east-1',
|
||||
externalId: 'my-id',
|
||||
},
|
||||
mainAccount: {
|
||||
profile: 'my-profile',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('does not fail when config is not set', () => {
|
||||
const output = readAwsIntegrationConfig(buildConfig({}));
|
||||
expect(output).toEqual({
|
||||
accountDefaults: {},
|
||||
accounts: [],
|
||||
mainAccount: {},
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects invalid combinations of account attributes', () => {
|
||||
const validAccount: any = {
|
||||
accountId: '111111111111',
|
||||
accessKeyId: 'ABC',
|
||||
secretAccessKey: 'EDF',
|
||||
roleName: 'hello',
|
||||
partition: 'aws',
|
||||
region: 'us-east-1',
|
||||
externalId: 'world',
|
||||
};
|
||||
expect(() =>
|
||||
readAwsIntegrationConfig(
|
||||
buildConfig({
|
||||
accounts: [
|
||||
validAccount,
|
||||
{
|
||||
accountId: '222222222222',
|
||||
accessKeyId: 'ABC',
|
||||
},
|
||||
],
|
||||
}),
|
||||
),
|
||||
).toThrow(/no secret access key/);
|
||||
expect(() =>
|
||||
readAwsIntegrationConfig(
|
||||
buildConfig({
|
||||
accounts: [
|
||||
validAccount,
|
||||
{
|
||||
accountId: '222222222222',
|
||||
secretAccessKey: 'ABC',
|
||||
},
|
||||
],
|
||||
}),
|
||||
),
|
||||
).toThrow(/no access key ID/);
|
||||
expect(() =>
|
||||
readAwsIntegrationConfig(
|
||||
buildConfig({
|
||||
accounts: [
|
||||
validAccount,
|
||||
{
|
||||
accountId: '222222222222',
|
||||
accessKeyId: 'ABC',
|
||||
secretAccessKey: 'DEF',
|
||||
profile: 'my-profile',
|
||||
},
|
||||
],
|
||||
}),
|
||||
),
|
||||
).toThrow(/only one must be specified/);
|
||||
expect(() =>
|
||||
readAwsIntegrationConfig(
|
||||
buildConfig({
|
||||
accounts: [
|
||||
validAccount,
|
||||
{
|
||||
accountId: '222222222222',
|
||||
roleName: 'my-role',
|
||||
profile: 'my-profile',
|
||||
},
|
||||
],
|
||||
}),
|
||||
),
|
||||
).toThrow(/only one must be specified/);
|
||||
expect(() =>
|
||||
readAwsIntegrationConfig(
|
||||
buildConfig({
|
||||
accounts: [
|
||||
validAccount,
|
||||
{
|
||||
accountId: '222222222222',
|
||||
partition: 'aws',
|
||||
},
|
||||
],
|
||||
}),
|
||||
),
|
||||
).toThrow(/no role name/);
|
||||
expect(() =>
|
||||
readAwsIntegrationConfig(
|
||||
buildConfig({
|
||||
accounts: [
|
||||
validAccount,
|
||||
{
|
||||
accountId: '222222222222',
|
||||
region: 'not-us-east-1',
|
||||
},
|
||||
],
|
||||
}),
|
||||
),
|
||||
).toThrow(/no role name/);
|
||||
expect(() =>
|
||||
readAwsIntegrationConfig(
|
||||
buildConfig({
|
||||
accounts: [
|
||||
validAccount,
|
||||
{
|
||||
accountId: '222222222222',
|
||||
externalId: 'hello',
|
||||
},
|
||||
],
|
||||
}),
|
||||
),
|
||||
).toThrow(/no role name/);
|
||||
});
|
||||
|
||||
it('rejects invalid combinations of main account attributes', () => {
|
||||
expect(() =>
|
||||
readAwsIntegrationConfig(
|
||||
buildConfig({
|
||||
mainAccount: {
|
||||
accessKeyId: 'ABC',
|
||||
},
|
||||
}),
|
||||
),
|
||||
).toThrow(/no secret access key/);
|
||||
expect(() =>
|
||||
readAwsIntegrationConfig(
|
||||
buildConfig({
|
||||
mainAccount: {
|
||||
secretAccessKey: 'ABC',
|
||||
},
|
||||
}),
|
||||
),
|
||||
).toThrow(/no access key ID/);
|
||||
expect(() =>
|
||||
readAwsIntegrationConfig(
|
||||
buildConfig({
|
||||
mainAccount: {
|
||||
accessKeyId: 'ABC',
|
||||
secretAccessKey: 'DEF',
|
||||
profile: 'my-profile',
|
||||
},
|
||||
}),
|
||||
),
|
||||
).toThrow(/only one must be specified/);
|
||||
});
|
||||
|
||||
it('rejects invalid combinations of account default attributes', () => {
|
||||
expect(() =>
|
||||
readAwsIntegrationConfig(
|
||||
buildConfig({
|
||||
accountDefaults: {
|
||||
partition: 'aws',
|
||||
},
|
||||
}),
|
||||
),
|
||||
).toThrow(/no role name/);
|
||||
expect(() =>
|
||||
readAwsIntegrationConfig(
|
||||
buildConfig({
|
||||
accountDefaults: {
|
||||
region: 'not-us-east-1',
|
||||
},
|
||||
}),
|
||||
),
|
||||
).toThrow(/no role name/);
|
||||
expect(() =>
|
||||
readAwsIntegrationConfig(
|
||||
buildConfig({
|
||||
accountDefaults: {
|
||||
externalId: 'hello',
|
||||
},
|
||||
}),
|
||||
),
|
||||
).toThrow(/no role name/);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,307 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Config } from '@backstage/config';
|
||||
|
||||
/**
|
||||
* The configuration parameters for a single AWS account for the AWS integration.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type AwsIntegrationAccountConfig = {
|
||||
/**
|
||||
* The account ID of the target account that this matches on, e.g. "123456789012"
|
||||
*/
|
||||
accountId: string;
|
||||
|
||||
/**
|
||||
* The access key ID for a set of static AWS credentials
|
||||
*/
|
||||
accessKeyId?: string;
|
||||
|
||||
/**
|
||||
* The secret access key for a set of static AWS credentials
|
||||
*/
|
||||
secretAccessKey?: string;
|
||||
|
||||
/**
|
||||
* The configuration profile from a credentials file at ~/.aws/credentials and
|
||||
* a configuration file at ~/.aws/config.
|
||||
*/
|
||||
profile?: string;
|
||||
|
||||
/**
|
||||
* The IAM role to assume to retrieve temporary AWS credentials
|
||||
*/
|
||||
roleName?: string;
|
||||
|
||||
/**
|
||||
* The AWS partition of the IAM role, e.g. "aws", "aws-cn"
|
||||
*/
|
||||
partition?: string;
|
||||
|
||||
/**
|
||||
* The STS regional endpoint to use when retrieving temporary AWS credentials, e.g. "ap-northeast-1"
|
||||
*/
|
||||
region?: string;
|
||||
|
||||
/**
|
||||
* The unique identifier needed to assume the role to retrieve temporary AWS credentials
|
||||
*/
|
||||
externalId?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* The configuration parameters for the main AWS account for the AWS integration.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type AwsIntegrationMainAccountConfig = {
|
||||
/**
|
||||
* The access key ID for a set of static AWS credentials
|
||||
*/
|
||||
accessKeyId?: string;
|
||||
|
||||
/**
|
||||
* The secret access key for a set of static AWS credentials
|
||||
*/
|
||||
secretAccessKey?: string;
|
||||
|
||||
/**
|
||||
* The configuration profile from a credentials file at ~/.aws/credentials and
|
||||
* a configuration file at ~/.aws/config.
|
||||
*/
|
||||
profile?: string;
|
||||
|
||||
/**
|
||||
* The STS regional endpoint to use for the main account, e.g. "ap-northeast-1"
|
||||
*/
|
||||
region?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* The default configuration parameters to use for accounts for the AWS integration.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type AwsIntegrationDefaultAccountConfig = {
|
||||
/**
|
||||
* The IAM role to assume to retrieve temporary AWS credentials
|
||||
*/
|
||||
roleName?: string;
|
||||
|
||||
/**
|
||||
* The AWS partition of the IAM role, e.g. "aws", "aws-cn"
|
||||
*/
|
||||
partition?: string;
|
||||
|
||||
/**
|
||||
* The STS regional endpoint to use when retrieving temporary AWS credentials, e.g. "ap-northeast-1"
|
||||
*/
|
||||
region?: string;
|
||||
|
||||
/**
|
||||
* The unique identifier needed to assume the role to retrieve temporary AWS credentials
|
||||
*/
|
||||
externalId?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* The configuration parameters for AWS account integration.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type AwsIntegrationConfig = {
|
||||
/**
|
||||
* Configuration for retrieving AWS accounts credentials
|
||||
*/
|
||||
accounts: AwsIntegrationAccountConfig[];
|
||||
|
||||
/**
|
||||
* Defaults for retrieving AWS account credentials
|
||||
*/
|
||||
accountDefaults: AwsIntegrationDefaultAccountConfig;
|
||||
|
||||
/**
|
||||
* Main account to use for retrieving AWS account credentials
|
||||
*/
|
||||
mainAccount: AwsIntegrationMainAccountConfig;
|
||||
};
|
||||
|
||||
/**
|
||||
* Reads an AWS integration account config.
|
||||
*
|
||||
* @param config - The config object of a single account
|
||||
*/
|
||||
function readAwsIntegrationAccountConfig(
|
||||
config: Config,
|
||||
): AwsIntegrationAccountConfig {
|
||||
const accountConfig = {
|
||||
accountId: config.getString('accountId'),
|
||||
accessKeyId: config.getOptionalString('accessKeyId'),
|
||||
secretAccessKey: config.getOptionalString('secretAccessKey'),
|
||||
profile: config.getOptionalString('profile'),
|
||||
roleName: config.getOptionalString('roleName'),
|
||||
region: config.getOptionalString('region'),
|
||||
partition: config.getOptionalString('partition'),
|
||||
externalId: config.getOptionalString('externalId'),
|
||||
};
|
||||
|
||||
// Validate that the account config has the right combination of attributes
|
||||
if (accountConfig.accessKeyId && !accountConfig.secretAccessKey) {
|
||||
throw new Error(
|
||||
`AWS integration account ${accountConfig.accountId} has an access key ID configured, but no secret access key.`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!accountConfig.accessKeyId && accountConfig.secretAccessKey) {
|
||||
throw new Error(
|
||||
`AWS integration account ${accountConfig.accountId} has a secret access key configured, but no access key ID`,
|
||||
);
|
||||
}
|
||||
|
||||
if (accountConfig.profile && accountConfig.accessKeyId) {
|
||||
throw new Error(
|
||||
`AWS integration account ${accountConfig.accountId} has both an access key ID and a profile configured, but only one must be specified`,
|
||||
);
|
||||
}
|
||||
|
||||
if (accountConfig.profile && accountConfig.roleName) {
|
||||
throw new Error(
|
||||
`AWS integration account ${accountConfig.accountId} has both an access key ID and a role name configured, but only one must be specified`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!accountConfig.roleName && accountConfig.externalId) {
|
||||
throw new Error(
|
||||
`AWS integration account ${accountConfig.accountId} has an external ID configured, but no role name.`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!accountConfig.roleName && accountConfig.region) {
|
||||
throw new Error(
|
||||
`AWS integration account ${accountConfig.accountId} has an STS region configured, but no role name.`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!accountConfig.roleName && accountConfig.partition) {
|
||||
throw new Error(
|
||||
`AWS integration account ${accountConfig.accountId} has an IAM partition configured, but no role name.`,
|
||||
);
|
||||
}
|
||||
|
||||
return accountConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the main AWS integration account config.
|
||||
*
|
||||
* @param config - The config object of the main account
|
||||
*/
|
||||
function readMainAwsIntegrationAccountConfig(
|
||||
config: Config,
|
||||
): AwsIntegrationMainAccountConfig {
|
||||
const mainAccountConfig = {
|
||||
accessKeyId: config.getOptionalString('accessKeyId'),
|
||||
secretAccessKey: config.getOptionalString('secretAccessKey'),
|
||||
profile: config.getOptionalString('profile'),
|
||||
region: config.getOptionalString('region'),
|
||||
};
|
||||
|
||||
// Validate that the account config has the right combination of attributes
|
||||
if (mainAccountConfig.accessKeyId && !mainAccountConfig.secretAccessKey) {
|
||||
throw new Error(
|
||||
`The main AWS integration account has an access key ID configured, but no secret access key.`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!mainAccountConfig.accessKeyId && mainAccountConfig.secretAccessKey) {
|
||||
throw new Error(
|
||||
`The main AWS integration account has a secret access key configured, but no access key ID`,
|
||||
);
|
||||
}
|
||||
|
||||
if (mainAccountConfig.profile && mainAccountConfig.accessKeyId) {
|
||||
throw new Error(
|
||||
`The main AWS integration account has both an access key ID and a profile configured, but only one must be specified`,
|
||||
);
|
||||
}
|
||||
|
||||
return mainAccountConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the default settings for retrieving credentials from AWS integration accounts.
|
||||
*
|
||||
* @param config - The config object of the default account settings
|
||||
*/
|
||||
function readAwsIntegrationAccountDefaultsConfig(
|
||||
config: Config,
|
||||
): AwsIntegrationDefaultAccountConfig {
|
||||
const defaultAccountConfig = {
|
||||
roleName: config.getOptionalString('roleName'),
|
||||
partition: config.getOptionalString('partition'),
|
||||
region: config.getOptionalString('region'),
|
||||
externalId: config.getOptionalString('externalId'),
|
||||
};
|
||||
|
||||
// Validate that the account config has the right combination of attributes
|
||||
if (!defaultAccountConfig.roleName && defaultAccountConfig.externalId) {
|
||||
throw new Error(
|
||||
`AWS integration account default configuration has an external ID configured, but no role name.`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!defaultAccountConfig.roleName && defaultAccountConfig.region) {
|
||||
throw new Error(
|
||||
`AWS integration account default configuration has an STS region configured, but no role name.`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!defaultAccountConfig.roleName && defaultAccountConfig.partition) {
|
||||
throw new Error(
|
||||
`AWS integration account default configuration has an IAM partition configured, but no role name.`,
|
||||
);
|
||||
}
|
||||
|
||||
return defaultAccountConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads an AWS integration configuration
|
||||
*
|
||||
* @param config - the integration config object
|
||||
* @public
|
||||
*/
|
||||
export function readAwsIntegrationConfig(config: Config): AwsIntegrationConfig {
|
||||
const accounts = config
|
||||
.getOptionalConfigArray('accounts')
|
||||
?.map(readAwsIntegrationAccountConfig);
|
||||
const mainAccount = config.has('mainAccount')
|
||||
? readMainAwsIntegrationAccountConfig(config.getConfig('mainAccount'))
|
||||
: {};
|
||||
const accountDefaults = config.has('accountDefaults')
|
||||
? readAwsIntegrationAccountDefaultsConfig(
|
||||
config.getConfig('accountDefaults'),
|
||||
)
|
||||
: {};
|
||||
|
||||
return {
|
||||
accounts: accounts ?? [],
|
||||
mainAccount,
|
||||
accountDefaults,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { readAwsIntegrationConfig } from './config';
|
||||
export type {
|
||||
AwsIntegrationConfig,
|
||||
AwsIntegrationAccountConfig,
|
||||
AwsIntegrationDefaultAccountConfig,
|
||||
AwsIntegrationMainAccountConfig,
|
||||
} from './config';
|
||||
export { DefaultAwsCredentialsProvider } from './DefaultAwsCredentialsProvider';
|
||||
export type {
|
||||
AwsCredentials,
|
||||
AwsCredentialsProvider,
|
||||
AwsCredentialsProviderOptions,
|
||||
} from './types';
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { AwsCredentialIdentityProvider } from '@aws-sdk/types';
|
||||
|
||||
/**
|
||||
* A set of credentials information for an AWS account.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type AwsCredentials = {
|
||||
accountId?: string;
|
||||
stsRegion?: string;
|
||||
provider: AwsCredentialIdentityProvider;
|
||||
};
|
||||
|
||||
/**
|
||||
* The options for specifying the AWS credentials to retrieve.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type AwsCredentialsProviderOptions = {
|
||||
/**
|
||||
* The AWS account ID, e.g. '0123456789012'
|
||||
*/
|
||||
accountId?: string;
|
||||
|
||||
/**
|
||||
* The resource ARN that will be accessed with the returned credentials.
|
||||
* If account ID or region are not specified, they will be inferred from the ARN.
|
||||
*/
|
||||
arn?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* This allows implementations to be provided to retrieve AWS credentials.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface AwsCredentialsProvider {
|
||||
/**
|
||||
* Get credentials for an AWS account.
|
||||
*/
|
||||
getCredentials(opts?: AwsCredentialsProviderOptions): Promise<AwsCredentials>;
|
||||
}
|
||||
Reference in New Issue
Block a user