New package for AWS integration node library
Signed-off-by: Clare Liguori <liguori@amazon.com>
This commit is contained in:
@@ -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);
|
||||
@@ -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
@@ -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/);
|
||||
});
|
||||
});
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user