From 2499f6cdefc52f7c781fc5c1ecbe7b8c3ec31be2 Mon Sep 17 00:00:00 2001 From: Jonah Back Date: Thu, 11 Feb 2021 11:00:24 -0800 Subject: [PATCH] feat: add support for assuming role in plugins that use AWS Signed-off-by: Jonah Back --- .changeset/sixty-chicken-ring.md | 7 ++ .../src/stages/publish/awsS3.ts | 33 ++++++-- plugins/catalog-backend/config.d.ts | 13 ++++ plugins/catalog-backend/package-lock.json | 76 +++++++++++++++++++ ...sOrganizationCloudAccountProcessor.test.ts | 6 +- .../AwsOrganizationCloudAccountProcessor.ts | 41 +++++++++- .../processors/awsOrganization/config.test.ts | 37 +++++++++ .../processors/awsOrganization/config.ts | 44 +++++++++++ plugins/techdocs/config.d.ts | 6 ++ 9 files changed, 253 insertions(+), 10 deletions(-) create mode 100644 .changeset/sixty-chicken-ring.md create mode 100644 plugins/catalog-backend/package-lock.json create mode 100644 plugins/catalog-backend/src/ingestion/processors/awsOrganization/config.test.ts create mode 100644 plugins/catalog-backend/src/ingestion/processors/awsOrganization/config.ts diff --git a/.changeset/sixty-chicken-ring.md b/.changeset/sixty-chicken-ring.md new file mode 100644 index 0000000000..0fff26d6f9 --- /dev/null +++ b/.changeset/sixty-chicken-ring.md @@ -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 diff --git a/packages/techdocs-common/src/stages/publish/awsS3.ts b/packages/techdocs-common/src/stages/publish/awsS3.ts index 8eaf03eb2f..b29d0dfb58 100644 --- a/packages/techdocs-common/src/stages/publish/awsS3.ts +++ b/packages/techdocs-common/src/stages/publish/awsS3.ts @@ -15,7 +15,7 @@ */ import path from 'path'; import express from 'express'; -import aws from 'aws-sdk'; +import aws, { Credentials } from 'aws-sdk'; import { ManagedUpload } from 'aws-sdk/clients/s3'; import { Logger } from 'winston'; import { Entity, EntityName } from '@backstage/catalog-model'; @@ -26,6 +26,7 @@ import fs from 'fs-extra'; import { Readable } from 'stream'; import JSON5 from 'json5'; import createLimiter from 'p-limit'; +import { CredentialsOptions } from 'aws-sdk/lib/credentials'; const streamToBuffer = (stream: Readable): Promise => { return new Promise((resolve, reject) => { @@ -61,9 +62,30 @@ export class AwsS3Publish implements PublisherBase { ); let accessKeyId = undefined; let secretAccessKey = undefined; + let awsCredentials: + | Credentials + | CredentialsOptions + | undefined = undefined; if (credentials) { - accessKeyId = credentials.getOptionalString('accessKeyId'); - secretAccessKey = credentials.getOptionalString('secretAccessKey'); + const roleArn = credentials.getOptionalString('roleArn'); + if (roleArn && aws.config.credentials instanceof Credentials) { + awsCredentials = new aws.ChainableTemporaryCredentials({ + masterCredentials: aws.config.credentials as Credentials, + params: { + RoleSessionName: 'backstage-aws-organization-processor', + RoleArn: roleArn, + }, + }); + } else { + accessKeyId = credentials.getOptionalString('accessKeyId'); + secretAccessKey = credentials.getOptionalString('secretAccessKey'); + if (accessKeyId && secretAccessKey) { + awsCredentials = { + accessKeyId, + secretAccessKey, + }; + } + } } // AWS Region is an optional config. If missing, default AWS env variable AWS_REGION @@ -76,10 +98,7 @@ export class AwsS3Publish implements PublisherBase { ...(credentials && accessKeyId && secretAccessKey && { - credentials: { - accessKeyId, - secretAccessKey, - }, + credentials: awsCredentials, }), ...(region && { region, diff --git a/plugins/catalog-backend/config.d.ts b/plugins/catalog-backend/config.d.ts index 7877f34dcf..b85846af2a 100644 --- a/plugins/catalog-backend/config.d.ts +++ b/plugins/catalog-backend/config.d.ts @@ -334,6 +334,19 @@ export interface Config { }>; }; + /** + * AwsOrganizationCloudAccountProcessor configuration + */ + awsOrganization?: { + providers: Array<{ + /** + * The role to be assumed by this processor + * + */ + roleArn?: string; + }>; + }; + /** * MicrosoftGraphOrgReaderProcessor configuration */ diff --git a/plugins/catalog-backend/package-lock.json b/plugins/catalog-backend/package-lock.json new file mode 100644 index 0000000000..a0474f89fc --- /dev/null +++ b/plugins/catalog-backend/package-lock.json @@ -0,0 +1,76 @@ +{ + "name": "@backstage/plugin-catalog-backend", + "version": "0.5.1", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", + "dev": true + }, + "create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, + "diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true + }, + "make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "source-map-support": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", + "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "ts-node": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-9.1.1.tgz", + "integrity": "sha512-hPlt7ZACERQGf03M253ytLY3dHbGNGrAq9qIHWUY9XHYl1z7wYngSr3OQ5xmui8o2AaxsONxIzjafLUiWBo1Fg==", + "dev": true, + "requires": { + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "source-map-support": "^0.5.17", + "yn": "3.1.1" + }, + "dependencies": { + "yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true + } + } + } + } +} diff --git a/plugins/catalog-backend/src/ingestion/processors/AwsOrganizationCloudAccountProcessor.test.ts b/plugins/catalog-backend/src/ingestion/processors/AwsOrganizationCloudAccountProcessor.test.ts index ba163fa0c3..596e62748f 100644 --- a/plugins/catalog-backend/src/ingestion/processors/AwsOrganizationCloudAccountProcessor.test.ts +++ b/plugins/catalog-backend/src/ingestion/processors/AwsOrganizationCloudAccountProcessor.test.ts @@ -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({ + providers: [], + logger: winston.createLogger(), + }); const location = { type: 'aws-cloud-accounts', target: '' }; const emit = jest.fn(); const listAccounts = jest.fn(); diff --git a/plugins/catalog-backend/src/ingestion/processors/AwsOrganizationCloudAccountProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/AwsOrganizationCloudAccountProcessor.ts index 0e988e7307..83fd89801f 100644 --- a/plugins/catalog-backend/src/ingestion/processors/AwsOrganizationCloudAccountProcessor.ts +++ b/plugins/catalog-backend/src/ingestion/processors/AwsOrganizationCloudAccountProcessor.ts @@ -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 '../../../../../packages/config/src'; +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,40 @@ 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() { + providers: AwsOrganizationProviderConfig[]; + + static fromConfig(config: Config, options: { logger: Logger }) { + const c = config.getOptionalConfig('catalog.processors.awsOrganization'); + return new AwsOrganizationCloudAccountProcessor({ + ...options, + providers: c ? readAwsOrganizationConfig(c) : [], + }); + } + + constructor(options: { + providers: AwsOrganizationProviderConfig[]; + logger: Logger; + }) { + this.providers = options.providers; + this.logger = options.logger; + let credentials = undefined; + if ( + this.providers.length > 0 && + this.providers[0].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.providers[0].roleArn, + }, + }); + } this.organizations = new AWS.Organizations({ + credentials, region: AWS_ORGANIZATION_REGION, }); // Only available in us-east-1 } diff --git a/plugins/catalog-backend/src/ingestion/processors/awsOrganization/config.test.ts b/plugins/catalog-backend/src/ingestion/processors/awsOrganization/config.test.ts new file mode 100644 index 0000000000..571d19c829 --- /dev/null +++ b/plugins/catalog-backend/src/ingestion/processors/awsOrganization/config.test.ts @@ -0,0 +1,37 @@ +/* + * 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 = { + providers: [ + { + roleArn: 'aws::arn::foo', + }, + ], + }; + const actual = readAwsOrganizationConfig(new ConfigReader(config)); + const expected = [ + { + roleArn: 'aws::arn::foo', + }, + ]; + expect(actual).toEqual(expected); + }); +}); diff --git a/plugins/catalog-backend/src/ingestion/processors/awsOrganization/config.ts b/plugins/catalog-backend/src/ingestion/processors/awsOrganization/config.ts new file mode 100644 index 0000000000..120079d679 --- /dev/null +++ b/plugins/catalog-backend/src/ingestion/processors/awsOrganization/config.ts @@ -0,0 +1,44 @@ +/* + * 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 providers: AwsOrganizationProviderConfig[] = []; + const providerConfigs = config.getOptionalConfigArray('providers') ?? []; + + for (const providerConfig of providerConfigs) { + const roleArn = providerConfig.getOptionalString('roleArn'); + + providers.push({ + roleArn, + }); + } + + return providers; +} diff --git a/plugins/techdocs/config.d.ts b/plugins/techdocs/config.d.ts index 1104d75237..d0c2ee2e6f 100644 --- a/plugins/techdocs/config.d.ts +++ b/plugins/techdocs/config.d.ts @@ -86,6 +86,12 @@ export interface Config { * @visibility secret */ secretAccessKey: string; + /** + * ARN of role to be assumed + * attr: 'roleArn' - accepts a string value + * @visibility secret + */ + roleArn: string; }; /** * (Required) Cloud Storage Bucket Name