Merge pull request #4500 from backjo/feature/aws-assume-role
feat: add support for assuming role in plugins that use AWS
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
---
|
||||
'@backstage/techdocs-common': patch
|
||||
'@backstage/plugin-catalog-backend': patch
|
||||
'@backstage/plugin-techdocs': patch
|
||||
---
|
||||
|
||||
Add support for assuming role in AWS integrations
|
||||
@@ -191,6 +191,27 @@ techdocs:
|
||||
Refer to the
|
||||
[official AWS documentation for obtaining the credentials](https://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/getting-your-credentials.html).
|
||||
|
||||
**3c. Authentication using an assumed role** Users with multiple AWS accounts
|
||||
may want to use a role for S3 storage that is in a different AWS account. Using
|
||||
the `roleArn` parameter as seen below, you can instruct the TechDocs publisher
|
||||
to assume a role before accessing S3.
|
||||
|
||||
```yaml
|
||||
techdocs:
|
||||
publisher:
|
||||
type: 'awsS3'
|
||||
awsS3:
|
||||
bucketName: 'name-of-techdocs-storage-bucket'
|
||||
region:
|
||||
$env: AWS_REGION
|
||||
credentials:
|
||||
roleArn: arn:aws:iam::123456789012:role/my-backstage-role
|
||||
```
|
||||
|
||||
Note: Assuming a role requires that primary credentials are already configured
|
||||
at `AWS.config.credentials`. Read more about
|
||||
[assuming roles in AWS](https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html).
|
||||
|
||||
**4. That's it!**
|
||||
|
||||
Your Backstage app is now ready to use AWS S3 for TechDocs, to store and read
|
||||
|
||||
@@ -15,12 +15,13 @@
|
||||
*/
|
||||
import { Entity, EntityName } from '@backstage/catalog-model';
|
||||
import { Config } from '@backstage/config';
|
||||
import aws from 'aws-sdk';
|
||||
import aws, { Credentials } from 'aws-sdk';
|
||||
import { ManagedUpload } from 'aws-sdk/clients/s3';
|
||||
import express from 'express';
|
||||
import fs from 'fs-extra';
|
||||
import JSON5 from 'json5';
|
||||
import createLimiter from 'p-limit';
|
||||
import { CredentialsOptions } from 'aws-sdk/lib/credentials';
|
||||
import path from 'path';
|
||||
import { Readable } from 'stream';
|
||||
import { Logger } from 'winston';
|
||||
@@ -59,14 +60,34 @@ export class AwsS3Publish implements PublisherBase {
|
||||
// https://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/loading-node-credentials-shared.html
|
||||
// 3. IAM Roles for EC2
|
||||
// https://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/loading-node-credentials-iam.html
|
||||
const credentials = config.getOptionalConfig(
|
||||
const credentialsConfig = config.getOptionalConfig(
|
||||
'techdocs.publisher.awsS3.credentials',
|
||||
);
|
||||
let accessKeyId = undefined;
|
||||
let secretAccessKey = undefined;
|
||||
if (credentials) {
|
||||
accessKeyId = credentials.getOptionalString('accessKeyId');
|
||||
secretAccessKey = credentials.getOptionalString('secretAccessKey');
|
||||
let credentials: Credentials | CredentialsOptions | undefined = undefined;
|
||||
if (credentialsConfig) {
|
||||
const roleArn = credentialsConfig.getOptionalString('roleArn');
|
||||
if (roleArn && aws.config.credentials instanceof Credentials) {
|
||||
credentials = new aws.ChainableTemporaryCredentials({
|
||||
masterCredentials: aws.config.credentials as Credentials,
|
||||
params: {
|
||||
RoleSessionName: 'backstage-aws-techdocs-s3-publisher',
|
||||
RoleArn: roleArn,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
accessKeyId = credentialsConfig.getOptionalString('accessKeyId');
|
||||
secretAccessKey = credentialsConfig.getOptionalString(
|
||||
'secretAccessKey',
|
||||
);
|
||||
if (accessKeyId && secretAccessKey) {
|
||||
credentials = {
|
||||
accessKeyId,
|
||||
secretAccessKey,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AWS Region is an optional config. If missing, default AWS env variable AWS_REGION
|
||||
@@ -74,14 +95,7 @@ export class AwsS3Publish implements PublisherBase {
|
||||
const region = config.getOptionalString('techdocs.publisher.awsS3.region');
|
||||
|
||||
const storageClient = new aws.S3({
|
||||
...(credentials &&
|
||||
accessKeyId &&
|
||||
secretAccessKey && {
|
||||
credentials: {
|
||||
accessKeyId,
|
||||
secretAccessKey,
|
||||
},
|
||||
}),
|
||||
credentials,
|
||||
...(region && {
|
||||
region,
|
||||
}),
|
||||
|
||||
Vendored
+13
@@ -334,6 +334,19 @@ export interface Config {
|
||||
}>;
|
||||
};
|
||||
|
||||
/**
|
||||
* AwsOrganizationCloudAccountProcessor configuration
|
||||
*/
|
||||
awsOrganization?: {
|
||||
provider: {
|
||||
/**
|
||||
* The role to be assumed by this processor
|
||||
*
|
||||
*/
|
||||
roleArn?: string;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* MicrosoftGraphOrgReaderProcessor configuration
|
||||
*/
|
||||
|
||||
+5
-1
@@ -15,10 +15,14 @@
|
||||
*/
|
||||
|
||||
import { AwsOrganizationCloudAccountProcessor } from './AwsOrganizationCloudAccountProcessor';
|
||||
import * as winston from 'winston';
|
||||
|
||||
describe('AwsOrganizationCloudAccountProcessor', () => {
|
||||
describe('readLocation', () => {
|
||||
const processor = new AwsOrganizationCloudAccountProcessor();
|
||||
const processor = new AwsOrganizationCloudAccountProcessor({
|
||||
provider: {},
|
||||
logger: winston.createLogger(),
|
||||
});
|
||||
const location = { type: 'aws-cloud-accounts', target: '' };
|
||||
const emit = jest.fn();
|
||||
const listAccounts = jest.fn();
|
||||
|
||||
+38
-2
@@ -14,11 +14,17 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { LocationSpec, ResourceEntityV1alpha1 } from '@backstage/catalog-model';
|
||||
import AWS, { Organizations } from 'aws-sdk';
|
||||
import AWS, { Credentials, Organizations } from 'aws-sdk';
|
||||
import { Account, ListAccountsResponse } from 'aws-sdk/clients/organizations';
|
||||
|
||||
import * as results from './results';
|
||||
import { CatalogProcessor, CatalogProcessorEmit } from './types';
|
||||
import { Config } from '@backstage/config';
|
||||
import { Logger } from 'winston';
|
||||
import {
|
||||
AwsOrganizationProviderConfig,
|
||||
readAwsOrganizationConfig,
|
||||
} from './awsOrganization/config';
|
||||
|
||||
const AWS_ORGANIZATION_REGION = 'us-east-1';
|
||||
const LOCATION_TYPE = 'aws-cloud-accounts';
|
||||
@@ -33,9 +39,39 @@ const ORGANIZATION_ANNOTATION: string = 'amazonaws.com/organization-id';
|
||||
* If custom authentication is needed, it can be achieved by configuring the global AWS.credentials object.
|
||||
*/
|
||||
export class AwsOrganizationCloudAccountProcessor implements CatalogProcessor {
|
||||
logger: Logger;
|
||||
organizations: Organizations;
|
||||
constructor() {
|
||||
provider: AwsOrganizationProviderConfig;
|
||||
|
||||
static fromConfig(config: Config, options: { logger: Logger }) {
|
||||
const c = config.getOptionalConfig('catalog.processors.awsOrganization');
|
||||
return new AwsOrganizationCloudAccountProcessor({
|
||||
...options,
|
||||
provider: c ? readAwsOrganizationConfig(c) : {},
|
||||
});
|
||||
}
|
||||
|
||||
constructor(options: {
|
||||
provider: AwsOrganizationProviderConfig;
|
||||
logger: Logger;
|
||||
}) {
|
||||
this.provider = options.provider;
|
||||
this.logger = options.logger;
|
||||
let credentials = undefined;
|
||||
if (
|
||||
this.provider.roleArn !== undefined &&
|
||||
AWS.config.credentials instanceof Credentials
|
||||
) {
|
||||
credentials = new AWS.ChainableTemporaryCredentials({
|
||||
masterCredentials: AWS.config.credentials as Credentials,
|
||||
params: {
|
||||
RoleSessionName: 'backstage-aws-organization-processor',
|
||||
RoleArn: this.provider.roleArn,
|
||||
},
|
||||
});
|
||||
}
|
||||
this.organizations = new AWS.Organizations({
|
||||
credentials,
|
||||
region: AWS_ORGANIZATION_REGION,
|
||||
}); // Only available in us-east-1
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 { ConfigReader } from '@backstage/config';
|
||||
import { readAwsOrganizationConfig } from './config';
|
||||
|
||||
describe('readAwsOrganizationConfig', () => {
|
||||
it('applies all of the defaults', () => {
|
||||
const config = {
|
||||
provider: {
|
||||
roleArn: 'aws::arn::foo',
|
||||
},
|
||||
};
|
||||
const actual = readAwsOrganizationConfig(new ConfigReader(config));
|
||||
const expected = {
|
||||
roleArn: 'aws::arn::foo',
|
||||
};
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 Organization Processor
|
||||
*/
|
||||
export type AwsOrganizationProviderConfig = {
|
||||
/**
|
||||
* The role to assume for the processor.
|
||||
*/
|
||||
roleArn?: string;
|
||||
};
|
||||
|
||||
export function readAwsOrganizationConfig(
|
||||
config: Config,
|
||||
): AwsOrganizationProviderConfig {
|
||||
const providerConfig = config.getOptionalConfig('provider');
|
||||
|
||||
const roleArn = providerConfig?.getOptionalString('roleArn');
|
||||
return {
|
||||
roleArn,
|
||||
};
|
||||
}
|
||||
Vendored
+5
@@ -65,6 +65,11 @@ export interface Config {
|
||||
* @visibility secret
|
||||
*/
|
||||
secretAccessKey: string;
|
||||
/**
|
||||
* ARN of role to be assumed
|
||||
* @visibility backend
|
||||
*/
|
||||
roleArn?: string;
|
||||
};
|
||||
/**
|
||||
* (Required) Cloud Storage Bucket Name
|
||||
|
||||
Reference in New Issue
Block a user