Merge pull request #4228 from backjo/feature/AwsIamAuthForKubernetes

feature: add aws iam auth translator for kubernetes
This commit is contained in:
Ben Lambert
2021-01-26 10:25:24 +01:00
committed by GitHub
10 changed files with 232 additions and 5 deletions
+3
View File
@@ -31,11 +31,13 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@aws-sdk/credential-provider-node": "^3.3.0",
"@backstage/backend-common": "^0.5.0",
"@backstage/catalog-model": "^0.7.0",
"@backstage/config": "^0.1.2",
"@kubernetes/client-node": "^0.13.2",
"@types/express": "^4.17.6",
"aws4": "^1.11.0",
"compression": "^1.7.4",
"cors": "^2.8.5",
"express": "^4.17.1",
@@ -50,6 +52,7 @@
},
"devDependencies": {
"@backstage/cli": "^0.4.7",
"@types/aws4": "^1.5.1",
"supertest": "^4.0.2"
},
"files": [
+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';
}[];
};
}