From 6811112282b2c5b0fd275f1335dae3af12e814f1 Mon Sep 17 00:00:00 2001 From: Jonah Back Date: Sat, 23 Jan 2021 16:00:18 -0800 Subject: [PATCH 1/2] feature: add aws iam auth translator for kubernetes --- .changeset/chatty-cooks-begin.md | 6 ++ plugins/kubernetes-backend/package.json | 3 + plugins/kubernetes-backend/schema.d.ts | 4 +- .../AwsIamKubernetesAuthTranslator.test.ts | 63 ++++++++++++++++ .../AwsIamKubernetesAuthTranslator.ts | 73 +++++++++++++++++++ .../KubernetesAuthTranslatorGenerator.test.ts | 8 ++ .../KubernetesAuthTranslatorGenerator.ts | 4 + plugins/kubernetes-backend/src/types/types.ts | 2 +- plugins/kubernetes/schema.d.ts | 4 +- yarn.lock | 70 ++++++++++++++++++ 10 files changed, 232 insertions(+), 5 deletions(-) create mode 100644 .changeset/chatty-cooks-begin.md create mode 100644 plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.test.ts create mode 100644 plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.ts diff --git a/.changeset/chatty-cooks-begin.md b/.changeset/chatty-cooks-begin.md new file mode 100644 index 0000000000..e32d22d532 --- /dev/null +++ b/.changeset/chatty-cooks-begin.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-kubernetes': patch +'@backstage/plugin-kubernetes-backend': patch +--- + +Add AWS auth provider for Kubernetes diff --git a/plugins/kubernetes-backend/package.json b/plugins/kubernetes-backend/package.json index 56c6707427..005708c059 100644 --- a/plugins/kubernetes-backend/package.json +++ b/plugins/kubernetes-backend/package.json @@ -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", diff --git a/plugins/kubernetes-backend/schema.d.ts b/plugins/kubernetes-backend/schema.d.ts index 862cb6d2ce..31b6a2022b 100644 --- a/plugins/kubernetes-backend/schema.d.ts +++ b/plugins/kubernetes-backend/schema.d.ts @@ -20,8 +20,8 @@ export interface Config { clusters: { url: string; name: string; - serviceAccountToken: string; - authProvider: 'serviceAccount'; + serviceAccountToken: string | undefined; + authProvider: 'aws' | 'google' | 'serviceAccount'; }[]; }; } diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.test.ts b/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.test.ts new file mode 100644 index 0000000000..4411658ee6 --- /dev/null +++ b/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.test.ts @@ -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'); + }); +}); diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.ts b/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.ts new file mode 100644 index 0000000000..3b5ea59f18 --- /dev/null +++ b/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.ts @@ -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) => (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 { + 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 { + const clusterDetailsWithAuthToken: ClusterDetails = Object.assign( + {}, + clusterDetails, + ); + + clusterDetailsWithAuthToken.serviceAccountToken = await this.getBearerToken( + clusterDetails.name, + ); + return clusterDetailsWithAuthToken; + } +} diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/KubernetesAuthTranslatorGenerator.test.ts b/plugins/kubernetes-backend/src/kubernetes-auth-translator/KubernetesAuthTranslatorGenerator.test.ts index 3b27eb012a..ec2f6ef120 100644 --- a/plugins/kubernetes-backend/src/kubernetes-auth-translator/KubernetesAuthTranslatorGenerator.test.ts +++ b/plugins/kubernetes-backend/src/kubernetes-auth-translator/KubernetesAuthTranslatorGenerator.test.ts @@ -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', diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/KubernetesAuthTranslatorGenerator.ts b/plugins/kubernetes-backend/src/kubernetes-auth-translator/KubernetesAuthTranslatorGenerator.ts index f7cfc92112..4ae2aff35a 100644 --- a/plugins/kubernetes-backend/src/kubernetes-auth-translator/KubernetesAuthTranslatorGenerator.ts +++ b/plugins/kubernetes-backend/src/kubernetes-auth-translator/KubernetesAuthTranslatorGenerator.ts @@ -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(); } diff --git a/plugins/kubernetes-backend/src/types/types.ts b/plugins/kubernetes-backend/src/types/types.ts index cf6576153b..86191c6d72 100644 --- a/plugins/kubernetes-backend/src/types/types.ts +++ b/plugins/kubernetes-backend/src/types/types.ts @@ -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'; diff --git a/plugins/kubernetes/schema.d.ts b/plugins/kubernetes/schema.d.ts index f3f3c98f8c..39598edb39 100644 --- a/plugins/kubernetes/schema.d.ts +++ b/plugins/kubernetes/schema.d.ts @@ -35,11 +35,11 @@ export interface Config { /** * @visibility secret */ - serviceAccountToken: string; + serviceAccountToken: string | undefined; /** * @visibility frontend */ - authProvider: 'serviceAccount'; + authProvider: 'aws' | 'google' | 'serviceAccount'; }[]; }; } diff --git a/yarn.lock b/yarn.lock index 6710c7e8e7..87637142c0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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" From 8387849f7ced3ee86c6ca3d4fde3da9d7c2d59cb Mon Sep 17 00:00:00 2001 From: Jonah Back Date: Sat, 23 Jan 2021 17:31:07 -0800 Subject: [PATCH 2/2] move types/aws4 to devDeps --- plugins/kubernetes-backend/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/kubernetes-backend/package.json b/plugins/kubernetes-backend/package.json index 0bf4487b54..eeb91de777 100644 --- a/plugins/kubernetes-backend/package.json +++ b/plugins/kubernetes-backend/package.json @@ -36,7 +36,6 @@ "@backstage/catalog-model": "^0.7.0", "@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", @@ -53,6 +52,7 @@ }, "devDependencies": { "@backstage/cli": "^0.4.7", + "@types/aws4": "^1.5.1", "supertest": "^4.0.2" }, "files": [