port k8s plugin from aws v2 to v3 client
Signed-off-by: Brian Fletcher <brian@roadie.io>
This commit is contained in:
@@ -34,6 +34,9 @@
|
||||
"clean": "backstage-cli package clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-crypto/sha256-js": "^3.0.0",
|
||||
"@aws-sdk/credential-providers": "^3.208.0",
|
||||
"@aws-sdk/signature-v4": "^3.208.0",
|
||||
"@azure/identity": "^2.0.4",
|
||||
"@backstage/backend-common": "workspace:^",
|
||||
"@backstage/catalog-client": "workspace:^",
|
||||
@@ -49,7 +52,6 @@
|
||||
"@kubernetes/client-node": "0.18.1",
|
||||
"@types/express": "^4.17.6",
|
||||
"@types/luxon": "^3.0.0",
|
||||
"aws-sdk": "^2.840.0",
|
||||
"aws4": "^1.11.0",
|
||||
"compression": "^1.7.4",
|
||||
"cors": "^2.8.5",
|
||||
@@ -71,7 +73,6 @@
|
||||
"@backstage/cli": "workspace:^",
|
||||
"@types/aws4": "^1.5.1",
|
||||
"@types/http-proxy-middleware": "^0.19.3",
|
||||
"aws-sdk-mock": "^5.2.1",
|
||||
"mock-fs": "^5.2.0",
|
||||
"msw": "^1.0.0",
|
||||
"supertest": "^6.1.3"
|
||||
|
||||
+79
-116
@@ -13,148 +13,111 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import AWS from 'aws-sdk';
|
||||
import AWSMock from 'aws-sdk-mock';
|
||||
import { AwsIamKubernetesAuthTranslator } from './AwsIamKubernetesAuthTranslator';
|
||||
|
||||
const awsError: AWS.AWSError = {
|
||||
code: '123',
|
||||
message: 'no way',
|
||||
name: 'nope',
|
||||
time: new Date(),
|
||||
};
|
||||
let presign = jest.fn(async () => ({
|
||||
hostname: 'https://example.com',
|
||||
query: {},
|
||||
path: '/asdf',
|
||||
}));
|
||||
|
||||
const fromEnv = jest.fn(() => {
|
||||
return {
|
||||
AccessId: 'asdf',
|
||||
};
|
||||
});
|
||||
const fromTemporaryCredentials = jest.fn();
|
||||
jest.mock('@aws-sdk/signature-v4', () => ({
|
||||
SignatureV4: jest.fn().mockImplementation(() => ({
|
||||
presign,
|
||||
})),
|
||||
}));
|
||||
|
||||
jest.mock('@aws-sdk/credential-providers', () => ({
|
||||
fromEnv: () => fromEnv(),
|
||||
fromTemporaryCredentials: (opts: any) => fromTemporaryCredentials(opts),
|
||||
}));
|
||||
|
||||
describe('AwsIamKubernetesAuthTranslator tests', () => {
|
||||
let role: any = undefined;
|
||||
const credentials: any = {
|
||||
accessKeyId: 'bloop',
|
||||
secretAccessKey: 'omg-so-secret',
|
||||
sessionToken: 'token',
|
||||
};
|
||||
|
||||
let assumeResponse: any = {
|
||||
Credentials: {
|
||||
AccessKeyId: credentials.accessKeyId,
|
||||
SecretAccessKey: credentials.secretAccessKey,
|
||||
SessionToken: credentials.sessionToken,
|
||||
},
|
||||
};
|
||||
|
||||
let mockedCredentials: any = undefined;
|
||||
|
||||
AWS.config.credentials = new AWS.Credentials(credentials);
|
||||
|
||||
AWSMock.setSDKInstance(AWS);
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
function executeTranslation() {
|
||||
AWSMock.mock('STS', 'assumeRole', (_params: any, callback: Function) => {
|
||||
callback(null, assumeResponse);
|
||||
});
|
||||
|
||||
beforeEach(() => {});
|
||||
it('returns a signed url for AWS credentials without assume role', async () => {
|
||||
const authTranslator = new AwsIamKubernetesAuthTranslator();
|
||||
|
||||
if (mockedCredentials) {
|
||||
jest
|
||||
.spyOn(authTranslator, 'awsGetCredentials')
|
||||
.mockImplementation(async () => mockedCredentials);
|
||||
}
|
||||
|
||||
const response = authTranslator.decorateClusterDetailsWithAuth({
|
||||
assumeRole: role,
|
||||
const authPromise = authTranslator.decorateClusterDetailsWithAuth({
|
||||
name: 'test-cluster',
|
||||
url: '',
|
||||
authProvider: 'aws',
|
||||
});
|
||||
|
||||
mockedCredentials = undefined;
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
it('returns a signed url for AWS credentials', async () => {
|
||||
// These credentials are not real.
|
||||
// Pulled from example in docs: https://docs.aws.amazon.com/general/latest/gr/aws-sec-cred-types.html
|
||||
AWS.config.credentials = new AWS.Credentials(
|
||||
'AKIAIOSFODNN7EXAMPLE',
|
||||
'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',
|
||||
expect(fromEnv).toHaveBeenCalledWith();
|
||||
expect((await authPromise).serviceAccountToken).toEqual(
|
||||
'k8s-aws-v1.aHR0cHM6Ly9odHRwczovL2V4YW1wbGUuY29tL2FzZGY_',
|
||||
);
|
||||
});
|
||||
|
||||
const response = await executeTranslation();
|
||||
expect(response.serviceAccountToken).toBeDefined();
|
||||
it('returns a signed url for AWS credentials with assume role', async () => {
|
||||
const authTranslator = new AwsIamKubernetesAuthTranslator();
|
||||
|
||||
const authPromise = authTranslator.decorateClusterDetailsWithAuth({
|
||||
assumeRole: 'asdf',
|
||||
name: 'test-cluster',
|
||||
url: '',
|
||||
authProvider: 'aws',
|
||||
});
|
||||
expect(fromTemporaryCredentials).toHaveBeenCalledWith({
|
||||
clientConfig: {
|
||||
region: 'us-east-1',
|
||||
},
|
||||
masterCredentials: fromEnv(),
|
||||
params: {
|
||||
ExternalId: undefined,
|
||||
RoleArn: 'asdf',
|
||||
},
|
||||
});
|
||||
expect((await authPromise).serviceAccountToken).toEqual(
|
||||
'k8s-aws-v1.aHR0cHM6Ly9odHRwczovL2V4YW1wbGUuY29tL2FzZGY_',
|
||||
);
|
||||
});
|
||||
|
||||
it('returns a signed url for AWS credentials and passes the external id', async () => {
|
||||
const authTranslator = new AwsIamKubernetesAuthTranslator();
|
||||
|
||||
const authPromise = authTranslator.decorateClusterDetailsWithAuth({
|
||||
assumeRole: 'SomeRole',
|
||||
externalId: 'external-id',
|
||||
name: 'test-cluster',
|
||||
url: '',
|
||||
authProvider: 'aws',
|
||||
});
|
||||
expect(fromTemporaryCredentials).toHaveBeenCalledWith({
|
||||
clientConfig: {
|
||||
region: 'us-east-1',
|
||||
},
|
||||
masterCredentials: fromEnv(),
|
||||
params: {
|
||||
ExternalId: 'external-id',
|
||||
RoleArn: 'SomeRole',
|
||||
},
|
||||
});
|
||||
expect((await authPromise).serviceAccountToken).toEqual(
|
||||
'k8s-aws-v1.aHR0cHM6Ly9odHRwczovL2V4YW1wbGUuY29tL2FzZGY_',
|
||||
);
|
||||
});
|
||||
|
||||
describe('When the credentials is failing', () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(AWS.config, 'getCredentials').mockImplementation(cb => {
|
||||
cb(awsError, null);
|
||||
presign = jest.fn(async () => {
|
||||
throw new Error('no way');
|
||||
});
|
||||
});
|
||||
it('throws the right error', async () => {
|
||||
const authTranslator = new AwsIamKubernetesAuthTranslator();
|
||||
await expect(
|
||||
authTranslator.decorateClusterDetailsWithAuth({
|
||||
assumeRole: role,
|
||||
name: 'test-cluster',
|
||||
url: '',
|
||||
authProvider: 'aws',
|
||||
}),
|
||||
).rejects.toEqual(awsError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('When the role is assumed', () => {
|
||||
// These credentials are not real.
|
||||
// Pulled from example in docs: https://docs.aws.amazon.com/general/latest/gr/aws-sec-cred-types.html
|
||||
AWS.config.credentials = new AWS.Credentials(
|
||||
'AKIAIOSFODNN7EXAMPLE',
|
||||
'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',
|
||||
);
|
||||
role = 'SomeRole';
|
||||
|
||||
describe('When the role is valid', () => {
|
||||
it('returns a signed url for AWS credentials', async () => {
|
||||
const response = await executeTranslation();
|
||||
expect(response.serviceAccountToken).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('When the role is invalid', () => {
|
||||
it('returns the original AWS credentials', async () => {
|
||||
assumeResponse = undefined;
|
||||
await expect(executeTranslation()).rejects.toThrow(
|
||||
/Unable to assume role:/,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('When no AWS creds are available', () => {
|
||||
it('throws unable to get AWS credentials', async () => {
|
||||
mockedCredentials = new Error();
|
||||
await expect(executeTranslation()).rejects.toThrow(
|
||||
'No AWS credentials found.',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('When invalid AWS creds are available', () => {
|
||||
it('throws credentials are invalid to get AWS credentials', async () => {
|
||||
const undefinedSecret: any = undefined;
|
||||
AWS.config.credentials = new AWS.Credentials(
|
||||
'AKIAIOSFODNN7EXAMPLE',
|
||||
undefinedSecret,
|
||||
);
|
||||
await expect(executeTranslation()).rejects.toThrow(
|
||||
'Invalid AWS credentials found.',
|
||||
);
|
||||
).rejects.toThrow('no way');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+57
-78
@@ -13,10 +13,14 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import AWS, { Credentials } from 'aws-sdk';
|
||||
import { sign } from 'aws4';
|
||||
import { AWSClusterDetails } from '../types/types';
|
||||
import { KubernetesAuthTranslator } from './types';
|
||||
import {
|
||||
fromEnv,
|
||||
fromTemporaryCredentials,
|
||||
} from '@aws-sdk/credential-providers';
|
||||
import { SignatureV4 } from '@aws-sdk/signature-v4';
|
||||
import { Sha256 } from '@aws-crypto/sha256-js';
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -28,6 +32,8 @@ export type SigningCreds = {
|
||||
sessionToken: string | undefined;
|
||||
};
|
||||
|
||||
const defaultRegion = 'us-east-1';
|
||||
|
||||
/**
|
||||
*
|
||||
* @public
|
||||
@@ -35,90 +41,63 @@ export type SigningCreds = {
|
||||
export class AwsIamKubernetesAuthTranslator
|
||||
implements KubernetesAuthTranslator
|
||||
{
|
||||
validCredentials(creds: SigningCreds): boolean {
|
||||
return (creds?.accessKeyId && creds?.secretAccessKey) as unknown as boolean;
|
||||
}
|
||||
|
||||
awsGetCredentials = async (): Promise<Credentials> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
AWS.config.getCredentials(err => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
return resolve(AWS.config.credentials as Credentials);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
async getCredentials(
|
||||
assumeRole?: string,
|
||||
externalId?: string,
|
||||
): Promise<SigningCreds> {
|
||||
const awsCreds = await this.awsGetCredentials();
|
||||
|
||||
if (!(awsCreds instanceof Credentials))
|
||||
throw new Error('No AWS credentials found.');
|
||||
|
||||
let creds: SigningCreds = {
|
||||
accessKeyId: awsCreds.accessKeyId,
|
||||
secretAccessKey: awsCreds.secretAccessKey,
|
||||
sessionToken: awsCreds.sessionToken,
|
||||
};
|
||||
|
||||
if (!this.validCredentials(creds))
|
||||
throw new Error('Invalid AWS credentials found.');
|
||||
if (!assumeRole) return creds;
|
||||
|
||||
try {
|
||||
const params: AWS.STS.Types.AssumeRoleRequest = {
|
||||
RoleArn: assumeRole,
|
||||
RoleSessionName: 'backstage-login',
|
||||
};
|
||||
if (externalId) params.ExternalId = externalId;
|
||||
|
||||
const assumedRole = await new AWS.STS().assumeRole(params).promise();
|
||||
|
||||
if (!assumedRole.Credentials) {
|
||||
throw new Error(`No credentials returned for role ${assumeRole}`);
|
||||
}
|
||||
|
||||
creds = {
|
||||
accessKeyId: assumedRole.Credentials.AccessKeyId,
|
||||
secretAccessKey: assumedRole.Credentials.SecretAccessKey,
|
||||
sessionToken: assumedRole.Credentials.SessionToken,
|
||||
};
|
||||
} catch (e) {
|
||||
console.warn(`There was an error assuming the role: ${e}`);
|
||||
throw new Error(`Unable to assume role: ${e}`);
|
||||
}
|
||||
return creds;
|
||||
}
|
||||
async getBearerToken(
|
||||
private async getBearerToken(
|
||||
clusterName: string,
|
||||
assumeRole?: string,
|
||||
externalId?: string,
|
||||
): Promise<string> {
|
||||
const credentials = await this.getCredentials(assumeRole, externalId);
|
||||
const region = process.env.AWS_REGION ?? defaultRegion;
|
||||
let credentials = fromEnv();
|
||||
if (assumeRole) {
|
||||
credentials = fromTemporaryCredentials({
|
||||
masterCredentials: credentials,
|
||||
clientConfig: {
|
||||
region,
|
||||
},
|
||||
params: {
|
||||
RoleArn: assumeRole,
|
||||
ExternalId: externalId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const request = {
|
||||
host: `sts.amazonaws.com`,
|
||||
path: `/?Action=GetCallerIdentity&Version=2011-06-15&X-Amz-Expires=60`,
|
||||
headers: {
|
||||
'x-k8s-aws-id': clusterName,
|
||||
const signer = new SignatureV4({
|
||||
credentials,
|
||||
region,
|
||||
service: 'sts',
|
||||
sha256: Sha256,
|
||||
});
|
||||
|
||||
const request = await signer.presign(
|
||||
{
|
||||
headers: {
|
||||
host: `sts.${region}.amazonaws.com`,
|
||||
'x-k8s-aws-id': clusterName,
|
||||
},
|
||||
hostname: `sts.${region}.amazonaws.com`,
|
||||
method: 'GET',
|
||||
path: '/',
|
||||
protocol: 'https:',
|
||||
query: {
|
||||
Action: 'GetCallerIdentity',
|
||||
Version: '2011-06-15',
|
||||
},
|
||||
},
|
||||
signQuery: true,
|
||||
};
|
||||
{ expiresIn: 0 },
|
||||
);
|
||||
|
||||
const signed = sign(request, credentials);
|
||||
const url = `https://${signed.host}${signed.path}`;
|
||||
const base64Url = Buffer.from(url, 'binary').toString('base64');
|
||||
const urlSafeBase64Url = base64Url
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/\//g, '_')
|
||||
.replace(/=+$/, '');
|
||||
const query = Object.keys(request?.query ?? {})
|
||||
.map(
|
||||
q =>
|
||||
`${encodeURIComponent(q)}=${encodeURIComponent(
|
||||
request.query?.[q] as string,
|
||||
)}`,
|
||||
)
|
||||
.join('&');
|
||||
|
||||
return `k8s-aws-v1.${urlSafeBase64Url}`;
|
||||
const url = `https://${request.hostname}${request.path}?${query}`;
|
||||
|
||||
return `k8s-aws-v1.${Buffer.from(url).toString('base64url')}`;
|
||||
}
|
||||
|
||||
async decorateClusterDetailsWithAuth(
|
||||
|
||||
@@ -1404,7 +1404,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@aws-sdk/signature-v4@npm:3.296.0":
|
||||
"@aws-sdk/signature-v4@npm:3.296.0, @aws-sdk/signature-v4@npm:^3.208.0":
|
||||
version: 3.296.0
|
||||
resolution: "@aws-sdk/signature-v4@npm:3.296.0"
|
||||
dependencies:
|
||||
@@ -6881,6 +6881,9 @@ __metadata:
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@backstage/plugin-kubernetes-backend@workspace:plugins/kubernetes-backend"
|
||||
dependencies:
|
||||
"@aws-crypto/sha256-js": ^3.0.0
|
||||
"@aws-sdk/credential-providers": ^3.208.0
|
||||
"@aws-sdk/signature-v4": ^3.208.0
|
||||
"@azure/identity": ^2.0.4
|
||||
"@backstage/backend-common": "workspace:^"
|
||||
"@backstage/backend-test-utils": "workspace:^"
|
||||
@@ -6900,8 +6903,6 @@ __metadata:
|
||||
"@types/express": ^4.17.6
|
||||
"@types/http-proxy-middleware": ^0.19.3
|
||||
"@types/luxon": ^3.0.0
|
||||
aws-sdk: ^2.840.0
|
||||
aws-sdk-mock: ^5.2.1
|
||||
aws4: ^1.11.0
|
||||
compression: ^1.7.4
|
||||
cors: ^2.8.5
|
||||
|
||||
Reference in New Issue
Block a user