New package for AWS integration node library

Signed-off-by: Clare Liguori <liguori@amazon.com>
This commit is contained in:
Clare Liguori
2022-10-27 13:10:41 -07:00
parent 89c0b0e6e6
commit 13278732f6
13 changed files with 2016 additions and 3 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/integration-aws-node': minor
---
New package for AWS integration node library
@@ -0,0 +1 @@
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
+157
View File
@@ -0,0 +1,157 @@
# @backstage/integration-aws-node
This package providers helpers for fetching AWS account credentials
to be used by AWS SDK clients in backend packages and plugins.
## Backstage app configuration
Users of plugins and packages that use this library
will configure their AWS account information and credentials in their
Backstage app config.
Users can configure IAM user credentials, IAM roles, and profile names
for their AWS accounts in their Backstage config.
If the AWS integration configuration is missing, the credentials provider
from this package will fall back to the AWS SDK default credentials chain for
resources in the main AWS account.
The default credentials chain for Node resolves credentials in the
following order of precedence:
1. Environment variables
2. SSO credentials from token cache
3. Web identity token credentials
4. Shared credentials files
5. The EC2/ECS Instance Metadata Service
See more about the AWS SDK default credentials chain in the
[AWS SDK for Javascript Developer Guide](https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/setting-credentials-node.html).
Configuration examples:
```yaml
aws:
# The main account is used as the source of credentials for calling
# the STS AssumeRole API to assume IAM roles in other AWS accounts.
# This section can be omitted to fall back to the AWS SDK's default creds chain.
mainAccount:
accessKeyId: ${MY_ACCESS_KEY_ID}
secretAccessKey: ${MY_SECRET_ACCESS_KEY}
# Account credentials can be configured individually per account
accounts:
# Credentials can come from a role in the account
- accountId: '111111111111'
roleName: 'my-iam-role-name'
externalId: 'my-external-id'
# Credentials can come from other AWS partitions
- accountId: '222222222222'
partition: 'aws-other'
roleName: 'my-iam-role-name'
# The STS region to use for the AssumeRole call
region: 'not-us-east-1'
# The creds to use when calling AssumeRole
accessKeyId: ${MY_ACCESS_KEY_ID_FOR_ANOTHER_PARTITION}
secretAccessKey: ${MY_SECRET_ACCESS_KEY_FOR_ANOTHER_PARTITION}
# Credentials can come from static credentials
- accountId: '333333333333'
accessKeyId: ${MY_OTHER_ACCESS_KEY_ID}
secretAccessKey: ${MY_OTHER_SECRET_ACCESS_KEY}
# Credentials can come from a profile in a shared config file on disk
- accountId: '444444444444'
profile: my-profile-name
# Credentials can come from the AWS SDK's default creds chain
- accountId: '555555555555'
# Credentials for accounts can fall back to a common role name.
# This is useful for account discovery use cases where the account
# IDs may not be known when writing the static config.
# If all accounts have a role with the same name, then the "accounts"
# section can be omitted entirely.
accountDefaults:
roleName: 'my-backstage-role'
externalId: 'my-id'
```
## Integrate new plugins
Backend plugins can provide an AWS ARN or account ID to this library in order to
retrieve a credentials provider for the relevant account that can be fed directly
to an AWS SDK client.
The AWS SDK for Javascript V3 must be used.
```typescript
const awsCredentialsProvider = DefaultAwsCredentialsProvider.fromConfig(config);
// provide the account ID explicitly
const creds = await awsCredentialsProvider.getCredentials({ accountId });
// OR extract the account ID from the ARN
const creds = await awsCredentialsProvider.getCredentials({ arn });
// OR provide neither to get main account's credentials
const creds = await awsCredentialsProvider.getCredentials({});
// Example constructing an AWS Proton client with the returned credentials provider
const client = new ProtonClient({
region,
credentialDefaultProvider: () => creds.provider,
});
```
Depending on the nature of your plguin, you may either have the user specify the
relevant ARN or account ID in a catalog entity annotation or in the static Backstage
app configuration for your plugin.
For example, you can create a new catalog entity annotation for your plugin:
```yaml
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
annotations:
# Plugin annotation to specify an AWS account ID
my-plugin.io/aws-account-id: '123456789012'
# Plugin annotation to specify the AWS ARN of a specific resource
my-other-plugin.io/aws-dynamodb-table: 'arn:aws:dynamodb:us-east-2:123456789012:table/example-table'
```
In your plugin, read the annotation value so that you can retrieve the credentials provider:
```typescript
const MY_AWS_ACCOUNT_ID_ANNOTATION = 'my-plugin.io/aws-account-id';
const getAwsAccountId = (entity: Entity) =>
entity.metadata.annotations?.[MY_AWS_ACCOUNT_ID_ANNOTATION]);
```
Alternatively, you can create a new configuration field for your plugin:
```yaml
# app-config.yaml
my-plugin:
# Statically configure the AWS account ID to use
awsAccountId: '123456789012'
my-other-plugin:
# Statically configure the AWS ARN of a specific resource
awsDynamoDbTable: 'arn:aws:dynamodb:us-east-2:123456789012:table/example-table'
```
In your plugin, read the configuration value so that you can retrieve the credentials provider:
```typescript
// Read an account ID from your plugin's configuration
const awsCredentialsProvider = DefaultAwsCredentialsProvider.fromConfig(config);
const accountId = config.getString('my-plugin.awsAccountId');
const creds = await awsCredentialsProvider.getCredentials({ accountId });
// Or, read an AWS ARN from your plugin's configuration
const awsCredentialsProvider = DefaultAwsCredentialsProvider.fromConfig(config);
const arn = config.getString('my-other-plugin.awsDynamoDbTable');
const creds = await awsCredentialsProvider.getCredentials({ arn });
```
## Links
- [The Backstage homepage](https://backstage.io)
@@ -0,0 +1,73 @@
## API Report File for "@backstage/integration-aws-node"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { AwsCredentialIdentityProvider } from '@aws-sdk/types';
import { Config } from '@backstage/config';
// @public
export type AwsCredentials = {
accountId?: string;
stsRegion?: string;
provider: AwsCredentialIdentityProvider;
};
// @public
export interface AwsCredentialsProvider {
getCredentials(opts?: AwsCredentialsProviderOptions): Promise<AwsCredentials>;
}
// @public
export type AwsCredentialsProviderOptions = {
accountId?: string;
arn?: string;
};
// @public
export type AwsIntegrationAccountConfig = {
accountId: string;
accessKeyId?: string;
secretAccessKey?: string;
profile?: string;
roleName?: string;
partition?: string;
region?: string;
externalId?: string;
};
// @public
export type AwsIntegrationConfig = {
accounts: AwsIntegrationAccountConfig[];
accountDefaults: AwsIntegrationDefaultAccountConfig;
mainAccount: AwsIntegrationMainAccountConfig;
};
// @public
export type AwsIntegrationDefaultAccountConfig = {
roleName?: string;
partition?: string;
region?: string;
externalId?: string;
};
// @public
export type AwsIntegrationMainAccountConfig = {
accessKeyId?: string;
secretAccessKey?: string;
profile?: string;
region?: string;
};
// @public
export class DefaultAwsCredentialsProvider implements AwsCredentialsProvider {
// (undocumented)
static fromConfig(config: Config): DefaultAwsCredentialsProvider;
getCredentials(opts?: AwsCredentialsProviderOptions): Promise<AwsCredentials>;
}
// @public
export function readAwsIntegrationConfig(config: Config): AwsIntegrationConfig;
// (No @packageDocumentation comment for this package)
```
+123
View File
@@ -0,0 +1,123 @@
/*
* 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 interface Config {
/** Configuration for access to AWS accounts */
aws?: {
/**
* Defaults for retrieving AWS account credentials
*/
accountDefaults?: {
/**
* 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
* @visibility secret
*/
externalId?: string;
};
/**
* Main account to use for retrieving AWS account credentials
*/
mainAccount?: {
/**
* The access key ID for a set of static AWS credentials
* @visibility secret
*/
accessKeyId?: string;
/**
* The secret access key for a set of static AWS credentials
* @visibility secret
*/
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;
};
/**
* Configuration for retrieving AWS accounts credentials
*/
accounts?: Array<{
/**
* 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
* @visibility secret
*/
accessKeyId?: string;
/**
* The secret access key for a set of static AWS credentials
* @visibility secret
*/
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
* @visibility secret
*/
externalId?: string;
}>;
};
}
@@ -0,0 +1,55 @@
{
"name": "@backstage/integration-aws-node",
"description": "Helpers for fetching AWS account credentials",
"version": "0.0.0",
"main": "src/index.ts",
"types": "src/index.ts",
"publishConfig": {
"access": "public",
"main": "dist/index.cjs.js",
"module": "dist/index.esm.js",
"types": "dist/index.d.ts"
},
"backstage": {
"role": "node-library"
},
"homepage": "https://backstage.io",
"repository": {
"type": "git",
"url": "https://github.com/backstage/backstage",
"directory": "packages/integration-aws-node"
},
"keywords": [
"backstage"
],
"license": "Apache-2.0",
"scripts": {
"build": "backstage-cli package build",
"lint": "backstage-cli package lint",
"test": "backstage-cli package test",
"prepack": "backstage-cli package prepack",
"postpack": "backstage-cli package postpack",
"clean": "backstage-cli package clean"
},
"dependencies": {
"@aws-sdk/client-sts": "^3.208.0",
"@aws-sdk/credential-provider-node": "^3.208.0",
"@aws-sdk/credential-providers": "^3.208.0",
"@aws-sdk/types": "^3.208.0",
"@aws-sdk/util-arn-parser": "^3.208.0",
"@backstage/config": "workspace:^",
"@backstage/errors": "workspace:^"
},
"devDependencies": {
"@backstage/cli": "workspace:^",
"@backstage/config-loader": "workspace:^",
"@backstage/test-utils": "workspace:^",
"aws-sdk-client-mock": "^2.0.0",
"aws-sdk-client-mock-jest": "^2.0.0"
},
"files": [
"dist",
"config.d.ts"
],
"configSchema": "config.d.ts"
}
@@ -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/);
});
});
+307
View File
@@ -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>;
}
+170 -3
View File
@@ -639,7 +639,7 @@ __metadata:
languageName: node
linkType: hard
"@aws-sdk/client-sts@npm:3.218.0":
"@aws-sdk/client-sts@npm:3.218.0, @aws-sdk/client-sts@npm:^3.208.0":
version: 3.218.0
resolution: "@aws-sdk/client-sts@npm:3.218.0"
dependencies:
@@ -748,7 +748,7 @@ __metadata:
languageName: node
linkType: hard
"@aws-sdk/credential-provider-node@npm:3.218.0":
"@aws-sdk/credential-provider-node@npm:3.218.0, @aws-sdk/credential-provider-node@npm:^3.208.0":
version: 3.218.0
resolution: "@aws-sdk/credential-provider-node@npm:3.218.0"
dependencies:
@@ -1386,7 +1386,7 @@ __metadata:
languageName: node
linkType: hard
"@aws-sdk/util-arn-parser@npm:3.208.0":
"@aws-sdk/util-arn-parser@npm:3.208.0, @aws-sdk/util-arn-parser@npm:^3.208.0":
version: 3.208.0
resolution: "@aws-sdk/util-arn-parser@npm:3.208.0"
dependencies:
@@ -4135,6 +4135,25 @@ __metadata:
languageName: unknown
linkType: soft
"@backstage/integration-aws-node@workspace:packages/integration-aws-node":
version: 0.0.0-use.local
resolution: "@backstage/integration-aws-node@workspace:packages/integration-aws-node"
dependencies:
"@aws-sdk/client-sts": ^3.208.0
"@aws-sdk/credential-provider-node": ^3.208.0
"@aws-sdk/credential-providers": ^3.208.0
"@aws-sdk/types": ^3.208.0
"@aws-sdk/util-arn-parser": ^3.208.0
"@backstage/cli": "workspace:^"
"@backstage/config": "workspace:^"
"@backstage/config-loader": "workspace:^"
"@backstage/errors": "workspace:^"
"@backstage/test-utils": "workspace:^"
aws-sdk-client-mock: ^2.0.0
aws-sdk-client-mock-jest: ^2.0.0
languageName: unknown
linkType: soft
"@backstage/integration-react@npm:^1.1.5":
version: 1.1.6
resolution: "@backstage/integration-react@npm:1.1.6"
@@ -10137,6 +10156,15 @@ __metadata:
languageName: node
linkType: hard
"@jest/expect-utils@npm:^28.1.3":
version: 28.1.3
resolution: "@jest/expect-utils@npm:28.1.3"
dependencies:
jest-get-type: ^28.0.2
checksum: 808ea3a68292a7e0b95490fdd55605c430b4cf209ea76b5b61bfb2a1badcb41bc046810fe4e364bd5fe04663978aa2bd73d8f8465a761dd7c655aeb44cf22987
languageName: node
linkType: hard
"@jest/expect-utils@npm:^29.3.1":
version: 29.3.1
resolution: "@jest/expect-utils@npm:29.3.1"
@@ -10219,6 +10247,15 @@ __metadata:
languageName: node
linkType: hard
"@jest/schemas@npm:^28.1.3":
version: 28.1.3
resolution: "@jest/schemas@npm:28.1.3"
dependencies:
"@sinclair/typebox": ^0.24.1
checksum: 3cf1d4b66c9c4ffda58b246de1ddcba8e6ad085af63dccdf07922511f13b68c0cc480a7bc620cb4f3099a6f134801c747e1df7bfc7a4ef4dceefbdea3e31e1de
languageName: node
linkType: hard
"@jest/schemas@npm:^29.0.0":
version: 29.0.0
resolution: "@jest/schemas@npm:29.0.0"
@@ -10299,6 +10336,20 @@ __metadata:
languageName: node
linkType: hard
"@jest/types@npm:^28.1.3":
version: 28.1.3
resolution: "@jest/types@npm:28.1.3"
dependencies:
"@jest/schemas": ^28.1.3
"@types/istanbul-lib-coverage": ^2.0.0
"@types/istanbul-reports": ^3.0.0
"@types/node": "*"
"@types/yargs": ^17.0.8
chalk: ^4.0.0
checksum: 1e258d9c063fcf59ebc91e46d5ea5984674ac7ae6cae3e50aa780d22b4405bf2c925f40350bf30013839eb5d4b5e521d956ddf8f3b7c78debef0e75a07f57350
languageName: node
linkType: hard
"@jest/types@npm:^29.3.1":
version: 29.3.1
resolution: "@jest/types@npm:29.3.1"
@@ -14168,6 +14219,16 @@ __metadata:
languageName: node
linkType: hard
"@types/jest@npm:^28.1.3":
version: 28.1.8
resolution: "@types/jest@npm:28.1.8"
dependencies:
expect: ^28.0.0
pretty-format: ^28.0.0
checksum: d4cd36158a3ae1d4b42cc48a77c95de74bc56b84cf81e09af3ee0399c34f4a7da8ab9e787570f10004bd642f9e781b0033c37327fbbf4a8e4b6e37e8ee3693a7
languageName: node
linkType: hard
"@types/jquery@npm:^3.3.34":
version: 3.5.14
resolution: "@types/jquery@npm:3.5.14"
@@ -16664,6 +16725,18 @@ __metadata:
languageName: node
linkType: hard
"aws-sdk-client-mock-jest@npm:^2.0.0":
version: 2.0.0
resolution: "aws-sdk-client-mock-jest@npm:2.0.0"
dependencies:
"@types/jest": ^28.1.3
tslib: ^2.1.0
peerDependencies:
aws-sdk-client-mock: 2.0.0
checksum: 57dc95b52f0c41166af44c743f298992f688ad55c4492fd2f09d7be59277c57a9c0ce408c8081b88358cb75c802922ca9d299e747797b023518ea57c01fa4ece
languageName: node
linkType: hard
"aws-sdk-client-mock@npm:^2.0.0":
version: 2.0.1
resolution: "aws-sdk-client-mock@npm:2.0.1"
@@ -20223,6 +20296,13 @@ __metadata:
languageName: node
linkType: hard
"diff-sequences@npm:^28.1.1":
version: 28.1.1
resolution: "diff-sequences@npm:28.1.1"
checksum: e2529036505567c7ca5a2dea86b6bcd1ca0e3ae63bf8ebf529b8a99cfa915bbf194b7021dc1c57361a4017a6d95578d4ceb29fabc3232a4f4cb866a2726c7690
languageName: node
linkType: hard
"diff-sequences@npm:^29.3.1":
version: 29.3.1
resolution: "diff-sequences@npm:29.3.1"
@@ -22015,6 +22095,19 @@ __metadata:
languageName: node
linkType: hard
"expect@npm:^28.0.0":
version: 28.1.3
resolution: "expect@npm:28.1.3"
dependencies:
"@jest/expect-utils": ^28.1.3
jest-get-type: ^28.0.2
jest-matcher-utils: ^28.1.3
jest-message-util: ^28.1.3
jest-util: ^28.1.3
checksum: 101e0090de300bcafedb7dbfd19223368a2251ce5fe0105bbb6de5720100b89fb6b64290ebfb42febc048324c76d6a4979cdc4b61eb77747857daf7a5de9b03d
languageName: node
linkType: hard
"expect@npm:^29.0.0, expect@npm:^29.3.1":
version: 29.3.1
resolution: "expect@npm:29.3.1"
@@ -25656,6 +25749,18 @@ __metadata:
languageName: node
linkType: hard
"jest-diff@npm:^28.1.3":
version: 28.1.3
resolution: "jest-diff@npm:28.1.3"
dependencies:
chalk: ^4.0.0
diff-sequences: ^28.1.1
jest-get-type: ^28.0.2
pretty-format: ^28.1.3
checksum: fa8583e0ccbe775714ce850b009be1b0f6b17a4b6759f33ff47adef27942ebc610dbbcc8a5f7cfb7f12b3b3b05afc9fb41d5f766674616025032ff1e4f9866e0
languageName: node
linkType: hard
"jest-diff@npm:^29.3.1":
version: 29.3.1
resolution: "jest-diff@npm:29.3.1"
@@ -25725,6 +25830,13 @@ __metadata:
languageName: node
linkType: hard
"jest-get-type@npm:^28.0.2":
version: 28.0.2
resolution: "jest-get-type@npm:28.0.2"
checksum: 5281d7c89bc8156605f6d15784f45074f4548501195c26e9b188742768f72d40948252d13230ea905b5349038865a1a8eeff0e614cc530ff289dfc41fe843abd
languageName: node
linkType: hard
"jest-get-type@npm:^29.2.0":
version: 29.2.0
resolution: "jest-get-type@npm:29.2.0"
@@ -25765,6 +25877,18 @@ __metadata:
languageName: node
linkType: hard
"jest-matcher-utils@npm:^28.1.3":
version: 28.1.3
resolution: "jest-matcher-utils@npm:28.1.3"
dependencies:
chalk: ^4.0.0
jest-diff: ^28.1.3
jest-get-type: ^28.0.2
pretty-format: ^28.1.3
checksum: 6b34f0cf66f6781e92e3bec97bf27796bd2ba31121e5c5997218d9adba6deea38a30df5203937d6785b68023ed95cbad73663cc9aad6fb0cb59aeb5813a58daf
languageName: node
linkType: hard
"jest-matcher-utils@npm:^29.3.1":
version: 29.3.1
resolution: "jest-matcher-utils@npm:29.3.1"
@@ -25777,6 +25901,23 @@ __metadata:
languageName: node
linkType: hard
"jest-message-util@npm:^28.1.3":
version: 28.1.3
resolution: "jest-message-util@npm:28.1.3"
dependencies:
"@babel/code-frame": ^7.12.13
"@jest/types": ^28.1.3
"@types/stack-utils": ^2.0.0
chalk: ^4.0.0
graceful-fs: ^4.2.9
micromatch: ^4.0.4
pretty-format: ^28.1.3
slash: ^3.0.0
stack-utils: ^2.0.3
checksum: 1f266854166dcc6900d75a88b54a25225a2f3710d463063ff1c99021569045c35c7d58557b25447a17eb3a65ce763b2f9b25550248b468a9d4657db365f39e96
languageName: node
linkType: hard
"jest-message-util@npm:^29.3.1":
version: 29.3.1
resolution: "jest-message-util@npm:29.3.1"
@@ -25942,6 +26083,20 @@ __metadata:
languageName: node
linkType: hard
"jest-util@npm:^28.1.3":
version: 28.1.3
resolution: "jest-util@npm:28.1.3"
dependencies:
"@jest/types": ^28.1.3
"@types/node": "*"
chalk: ^4.0.0
ci-info: ^3.2.0
graceful-fs: ^4.2.9
picomatch: ^2.2.3
checksum: fd6459742c941f070223f25e38a2ac0719aad92561591e9fb2a50d602a5d19d754750b79b4074327a42b00055662b95da3b006542ceb8b54309da44d4a62e721
languageName: node
linkType: hard
"jest-util@npm:^29.3.1":
version: 29.3.1
resolution: "jest-util@npm:29.3.1"
@@ -31752,6 +31907,18 @@ __metadata:
languageName: node
linkType: hard
"pretty-format@npm:^28.0.0, pretty-format@npm:^28.1.3":
version: 28.1.3
resolution: "pretty-format@npm:28.1.3"
dependencies:
"@jest/schemas": ^28.1.3
ansi-regex: ^5.0.1
ansi-styles: ^5.0.0
react-is: ^18.0.0
checksum: e69f857358a3e03d271252d7524bec758c35e44680287f36c1cb905187fbc82da9981a6eb07edfd8a03bc3cbeebfa6f5234c13a3d5b59f2bbdf9b4c4053e0a7f
languageName: node
linkType: hard
"pretty-format@npm:^29.0.0, pretty-format@npm:^29.3.1":
version: 29.3.1
resolution: "pretty-format@npm:29.3.1"