feat: add support for assuming role in plugins that use AWS

Signed-off-by: Jonah Back <jback@legalzoom.com>
This commit is contained in:
Jonah Back
2021-02-11 11:00:24 -08:00
parent 1b38e38094
commit 2499f6cdef
9 changed files with 253 additions and 10 deletions
+7
View File
@@ -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
@@ -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<Buffer> => {
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,
+13
View File
@@ -334,6 +334,19 @@ export interface Config {
}>;
};
/**
* AwsOrganizationCloudAccountProcessor configuration
*/
awsOrganization?: {
providers: Array<{
/**
* The role to be assumed by this processor
*
*/
roleArn?: string;
}>;
};
/**
* MicrosoftGraphOrgReaderProcessor configuration
*/
+76
View File
@@ -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
}
}
}
}
}
@@ -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();
@@ -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
}
@@ -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);
});
});
@@ -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;
}
+6
View File
@@ -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