feature: add aws iam auth translator for kubernetes

This commit is contained in:
Jonah Back
2021-01-23 16:00:18 -08:00
parent 7abe2c2c33
commit 6811112282
10 changed files with 232 additions and 5 deletions
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/plugin-kubernetes': patch
'@backstage/plugin-kubernetes-backend': patch
---
Add AWS auth provider for Kubernetes
+3
View File
@@ -31,11 +31,14 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@aws-sdk/credential-provider-node": "^3.3.0",
"@backstage/backend-common": "^0.4.3",
"@backstage/catalog-model": "^0.6.1",
"@backstage/config": "^0.1.2",
"@kubernetes/client-node": "^0.13.2",
"@types/aws4": "^1.5.1",
"@types/express": "^4.17.6",
"aws4": "^1.11.0",
"compression": "^1.7.4",
"cors": "^2.8.5",
"express": "^4.17.1",
+2 -2
View File
@@ -20,8 +20,8 @@ export interface Config {
clusters: {
url: string;
name: string;
serviceAccountToken: string;
authProvider: 'serviceAccount';
serviceAccountToken: string | undefined;
authProvider: 'aws' | 'google' | 'serviceAccount';
}[];
};
}
@@ -0,0 +1,63 @@
/*
* 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.
*/
const mockCredentialProvider = jest.fn();
jest.mock('@aws-sdk/credential-provider-node', () => {
return {
defaultProvider: () => mockCredentialProvider,
};
});
import { AwsIamKubernetesAuthTranslator } from './AwsIamKubernetesAuthTranslator';
describe('AwsIamKubernetesAuthTranslator tests', () => {
beforeEach(() => {
jest.resetAllMocks();
});
it('returns a signed url for aws credentials', async () => {
const authTranslator = new AwsIamKubernetesAuthTranslator();
mockCredentialProvider.mockImplementation(async () => {
// These credentials are not real.
// Pulled from example in docs: https://docs.aws.amazon.com/general/latest/gr/aws-sec-cred-types.html
return {
accessKeyId: 'AKIAIOSFODNN7EXAMPLE',
secretKeyId: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',
};
});
const clusterDetails = await authTranslator.decorateClusterDetailsWithAuth({
name: 'test-cluster',
url: '',
authProvider: 'aws',
});
expect(clusterDetails.serviceAccountToken).toBeDefined();
});
it('throws when unable to get aws credentials', async () => {
const authTranslator = new AwsIamKubernetesAuthTranslator();
mockCredentialProvider.mockImplementation(async () => {
throw new Error('not implemented');
});
const promise = authTranslator.decorateClusterDetailsWithAuth({
name: 'test-cluster',
url: '',
authProvider: 'aws',
});
await expect(promise).rejects.toThrow('not implemented');
});
});
@@ -0,0 +1,73 @@
/*
* 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 { defaultProvider } from '@aws-sdk/credential-provider-node';
import { sign } from 'aws4';
import { KubernetesAuthTranslator } from './types';
import { ClusterDetails } from '..';
const base64 = (str: string) =>
Buffer.from(str.toString(), 'binary').toString('base64');
const prepend = (prep: string) => (str: string) => prep + str;
const replace = (search: string | RegExp, substitution: string) => (
str: string,
) => str.replace(search, substitution);
const pipe = (fns: ReadonlyArray<any>) => (thing: string): string =>
fns.reduce((val, fn) => fn(val), thing);
const removePadding = replace(/=+$/, '');
const makeUrlSafe = pipe([replace('+', '-'), replace('/', '_')]);
export class AwsIamKubernetesAuthTranslator
implements KubernetesAuthTranslator {
async getBearerToken(clusterName: string): Promise<string> {
const credentialProvider = defaultProvider();
const credentials = await credentialProvider();
const request = {
host: `sts.amazonaws.com`,
path: `/?Action=GetCallerIdentity&Version=2011-06-15&X-Amz-Expires=60`,
headers: {
'x-k8s-aws-id': clusterName,
},
signQuery: true,
};
const signedRequest = sign(request, {
accessKeyId: credentials.accessKeyId,
secretAccessKey: credentials.secretAccessKey,
sessionToken: credentials.sessionToken,
});
return pipe([
(signed: any) => `https://${signed.host}${signed.path}`,
base64,
removePadding,
makeUrlSafe,
prepend('k8s-aws-v1.'),
])(signedRequest);
}
async decorateClusterDetailsWithAuth(
clusterDetails: ClusterDetails,
): Promise<ClusterDetails> {
const clusterDetailsWithAuthToken: ClusterDetails = Object.assign(
{},
clusterDetails,
);
clusterDetailsWithAuthToken.serviceAccountToken = await this.getBearerToken(
clusterDetails.name,
);
return clusterDetailsWithAuthToken;
}
}
@@ -18,6 +18,7 @@ import { KubernetesAuthTranslator } from './types';
import { GoogleKubernetesAuthTranslator } from './GoogleKubernetesAuthTranslator';
import { KubernetesAuthTranslatorGenerator } from './KubernetesAuthTranslatorGenerator';
import { ServiceAccountKubernetesAuthTranslator } from './ServiceAccountKubernetesAuthTranslator';
import { AwsIamKubernetesAuthTranslator } from './AwsIamKubernetesAuthTranslator';
describe('getKubernetesAuthTranslatorInstance', () => {
const sut = KubernetesAuthTranslatorGenerator;
@@ -29,6 +30,13 @@ describe('getKubernetesAuthTranslatorInstance', () => {
expect(authTranslator instanceof GoogleKubernetesAuthTranslator).toBe(true);
});
it('can return an auth translator for aws auth', () => {
const authTranslator: KubernetesAuthTranslator = sut.getKubernetesAuthTranslatorInstance(
'aws',
);
expect(authTranslator instanceof AwsIamKubernetesAuthTranslator).toBe(true);
});
it('can return an auth translator for serviceAccount auth', () => {
const authTranslator: KubernetesAuthTranslator = sut.getKubernetesAuthTranslatorInstance(
'serviceAccount',
@@ -17,6 +17,7 @@
import { KubernetesAuthTranslator } from './types';
import { GoogleKubernetesAuthTranslator } from './GoogleKubernetesAuthTranslator';
import { ServiceAccountKubernetesAuthTranslator } from './ServiceAccountKubernetesAuthTranslator';
import { AwsIamKubernetesAuthTranslator } from './AwsIamKubernetesAuthTranslator';
export class KubernetesAuthTranslatorGenerator {
static getKubernetesAuthTranslatorInstance(
@@ -26,6 +27,9 @@ export class KubernetesAuthTranslatorGenerator {
case 'google': {
return new GoogleKubernetesAuthTranslator();
}
case 'aws': {
return new AwsIamKubernetesAuthTranslator();
}
case 'serviceAccount': {
return new ServiceAccountKubernetesAuthTranslator();
}
@@ -148,4 +148,4 @@ export interface KubernetesFetchError {
export type ServiceLocatorMethod = 'multiTenant' | 'http'; // TODO implement http
export type ClusterLocatorMethod = 'config';
export type AuthProviderType = 'google' | 'serviceAccount';
export type AuthProviderType = 'google' | 'serviceAccount' | 'aws';
+2 -2
View File
@@ -35,11 +35,11 @@ export interface Config {
/**
* @visibility secret
*/
serviceAccountToken: string;
serviceAccountToken: string | undefined;
/**
* @visibility frontend
*/
authProvider: 'serviceAccount';
authProvider: 'aws' | 'google' | 'serviceAccount';
}[];
};
}
+70
View File
@@ -263,6 +263,15 @@
"@aws-sdk/property-provider" "3.1.0"
tslib "^1.8.0"
"@aws-sdk/credential-provider-env@3.3.0":
version "3.3.0"
resolved "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.3.0.tgz#7930e504a7a79ab98a9fd34befc5c84b8c4df679"
integrity sha512-kyqZMlGdH/05IhuXLBUXtj5+hhRfYiHFcJLc3ts/uiwCixswVHPAYHgyWm9ajFkmWtpz6ih+0LoYryhPbYu01A==
dependencies:
"@aws-sdk/property-provider" "3.3.0"
"@aws-sdk/types" "3.1.0"
tslib "^1.8.0"
"@aws-sdk/credential-provider-imds@3.1.0":
version "3.1.0"
resolved "https://registry.npmjs.org/@aws-sdk/credential-provider-imds/-/credential-provider-imds-3.1.0.tgz#33d48753dc00bddce79d2aa8076a7cb5bf8562df"
@@ -271,6 +280,15 @@
"@aws-sdk/property-provider" "3.1.0"
tslib "^1.8.0"
"@aws-sdk/credential-provider-imds@3.3.0":
version "3.3.0"
resolved "https://registry.npmjs.org/@aws-sdk/credential-provider-imds/-/credential-provider-imds-3.3.0.tgz#ff0cf5489853c16d23fc99d7bae425587e836c40"
integrity sha512-Cx0YMnO/ScGQVDns006bLbqOxNURGN2Xm21bCY0l0ZUJCdJ2va1/9q1rljDyw2KvdzZNQVRQII3uUgj/Oq/K+g==
dependencies:
"@aws-sdk/property-provider" "3.3.0"
"@aws-sdk/types" "3.1.0"
tslib "^1.8.0"
"@aws-sdk/credential-provider-ini@3.1.0":
version "3.1.0"
resolved "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.1.0.tgz#1a6cf9ab9fa1450d4472b9e371099b0c0283349b"
@@ -280,6 +298,16 @@
"@aws-sdk/shared-ini-file-loader" "3.1.0"
tslib "^1.8.0"
"@aws-sdk/credential-provider-ini@3.3.0":
version "3.3.0"
resolved "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.3.0.tgz#55fe8f391b72d30e650ba8bc680e82bbeacbbfe5"
integrity sha512-zawNFJoasXiaV5n0H3/KNOi7mAZ7mHpG1+nBEkoWhZ31lIUM9+heGPcxKCbf/pMQjiOebUqL1OpWe4uSWxIVMw==
dependencies:
"@aws-sdk/property-provider" "3.3.0"
"@aws-sdk/shared-ini-file-loader" "3.1.0"
"@aws-sdk/types" "3.1.0"
tslib "^1.8.0"
"@aws-sdk/credential-provider-node@3.1.0":
version "3.1.0"
resolved "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.1.0.tgz#89bc8803752a3e580c6f2410306c7edad6be7fa2"
@@ -292,6 +320,19 @@
"@aws-sdk/property-provider" "3.1.0"
tslib "^1.8.0"
"@aws-sdk/credential-provider-node@^3.3.0":
version "3.3.0"
resolved "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.3.0.tgz#5c97323fa7b23590070d06aa7b1be8d93b2bf4be"
integrity sha512-PPBNzPq8fHk9dEQTTE4iJi6ZWtmo057Lc+I8Rlzmvz6NthK9iKiU819tfaxVBb6ZR7bLP0BuDiCi4G1lD+rQnQ==
dependencies:
"@aws-sdk/credential-provider-env" "3.3.0"
"@aws-sdk/credential-provider-imds" "3.3.0"
"@aws-sdk/credential-provider-ini" "3.3.0"
"@aws-sdk/credential-provider-process" "3.3.0"
"@aws-sdk/property-provider" "3.3.0"
"@aws-sdk/types" "3.1.0"
tslib "^1.8.0"
"@aws-sdk/credential-provider-process@3.1.0":
version "3.1.0"
resolved "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.1.0.tgz#ff817b29a9760c463b77be3ce49375eaeb753ef3"
@@ -302,6 +343,17 @@
"@aws-sdk/shared-ini-file-loader" "3.1.0"
tslib "^1.8.0"
"@aws-sdk/credential-provider-process@3.3.0":
version "3.3.0"
resolved "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.3.0.tgz#9de0984bd6dd0f5e40cff3672d7dd19e8cd43074"
integrity sha512-7oOF1j6ydUq43P3SsasiIpbxMKCmT0C+XwggHTGiVxNtX+QZiH1vdMf8otA7puLEey0iY5wTAIEcZhC6HenojA==
dependencies:
"@aws-sdk/credential-provider-ini" "3.3.0"
"@aws-sdk/property-provider" "3.3.0"
"@aws-sdk/shared-ini-file-loader" "3.1.0"
"@aws-sdk/types" "3.1.0"
tslib "^1.8.0"
"@aws-sdk/eventstream-marshaller@3.1.0":
version "3.1.0"
resolved "https://registry.npmjs.org/@aws-sdk/eventstream-marshaller/-/eventstream-marshaller-3.1.0.tgz#65a217e37abcaa162276ccb1d4487d42431d1534"
@@ -634,6 +686,14 @@
dependencies:
tslib "^1.8.0"
"@aws-sdk/property-provider@3.3.0":
version "3.3.0"
resolved "https://registry.npmjs.org/@aws-sdk/property-provider/-/property-provider-3.3.0.tgz#49979cb1a3e5562d51807c7403c5fd48cb9f2cdc"
integrity sha512-JTyjtXVNhFczL9IfgwXD55F6DqXL50PhfZxFW92t5dDj5VtWpOL74BbuxHQxHBgnQv1FKLr6N9cr7gfXWexDug==
dependencies:
"@aws-sdk/types" "3.1.0"
tslib "^1.8.0"
"@aws-sdk/protocol-http@3.1.0":
version "3.1.0"
resolved "https://registry.npmjs.org/@aws-sdk/protocol-http/-/protocol-http-3.1.0.tgz#7da0ebcf02a40a8300f3bd52f9206f25fdf1ca7f"
@@ -6147,6 +6207,11 @@
resolved "https://registry.npmjs.org/@types/aria-query/-/aria-query-4.2.0.tgz#14264692a9d6e2fa4db3df5e56e94b5e25647ac0"
integrity sha512-iIgQNzCm0v7QMhhe4Jjn9uRh+I6GoPmt03CbEtwx3ao8/EfoQcmgtqH4vQ5Db/lxiIGaWDv6nwvunuh0RyX0+A==
"@types/aws4@^1.5.1":
version "1.5.1"
resolved "https://registry.npmjs.org/@types/aws4/-/aws4-1.5.1.tgz#361fadab198a030ab398269183ae3fa86e958ed9"
integrity sha1-Nh+tqxmKAwqzmCaRg64/qG6Vjtk=
"@types/babel__core@^7.0.0", "@types/babel__core@^7.1.7":
version "7.1.9"
resolved "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.9.tgz#77e59d438522a6fb898fa43dc3455c6e72f3963d"
@@ -8580,6 +8645,11 @@ aws-sign2@~0.7.0:
resolved "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8"
integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=
aws4@^1.11.0:
version "1.11.0"
resolved "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz#d61f46d83b2519250e2784daf5b09479a8b41c59"
integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==
aws4@^1.8.0:
version "1.9.1"
resolved "https://registry.npmjs.org/aws4/-/aws4-1.9.1.tgz#7e33d8f7d449b3f673cd72deb9abdc552dbe528e"