Skip to content

Commit

Permalink
mid work
Browse files Browse the repository at this point in the history
  • Loading branch information
iliapolo committed Aug 31, 2024
1 parent 9ba3fd1 commit bb4f087
Show file tree
Hide file tree
Showing 6 changed files with 158 additions and 19 deletions.
29 changes: 17 additions & 12 deletions lib/aws.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,17 @@ export interface IAws {
secretsManagerClient(options: ClientOptions): Promise<AWS.SecretsManager>;
}

// Partial because `RoleSessionName` is required in STS, but we have a default value for it.
export type AssumeRoleAdditionalOptions = Partial<
// cloud-assembly-schema validates that `ExternalId` and `RoleArn` are not configured
Omit<AWS.STS.Types.AssumeRoleRequest, 'ExternalId' | 'RoleArn'>
>;

export interface ClientOptions {
region?: string;
assumeRoleArn?: string;
assumeRoleExternalId?: string;
assumeRoleSessionTags?: { [key: string]: string };
assumeRoleAdditionalOptions?: AssumeRoleAdditionalOptions;
quiet?: boolean;
}

Expand Down Expand Up @@ -120,7 +126,7 @@ export class DefaultAwsClient implements IAws {
options.region,
options.assumeRoleArn,
options.assumeRoleExternalId,
options.assumeRoleSessionTags
options.assumeRoleAdditionalOptions
);
}

Expand All @@ -143,22 +149,21 @@ export class DefaultAwsClient implements IAws {
region: string | undefined,
roleArn: string,
externalId?: string,
sessionTags?: { [key: string]: string }
additionalOptions?: AssumeRoleAdditionalOptions
): Promise<AWS.Credentials> {
const parsedTags = sessionTags
? Object.entries(sessionTags).map(([key, value]) => ({
Key: key,
Value: value,
}))
: [];

if (
additionalOptions?.Tags &&
additionalOptions.Tags.length > 0 &&
!additionalOptions.TransitiveTagKeys
) {
additionalOptions.TransitiveTagKeys = additionalOptions.Tags?.map((t) => t.Key);
}
return new this.AWS.ChainableTemporaryCredentials({
params: {
RoleArn: roleArn,
ExternalId: externalId,
RoleSessionName: `cdk-assets-${safeUsername()}`,
Tags: parsedTags,
TransitiveTagKeys: sessionTags ? Object.keys(sessionTags) : [],
// ...(additionalOptions ?? {}),
},
stsConfig: {
region,
Expand Down
5 changes: 3 additions & 2 deletions lib/private/handlers/container-images.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,8 @@ export class ContainerImageAssetHandler implements IAssetHandler {
const ecr = await this.host.aws.ecrClient({
assumeRoleArn: destination.assumeRoleArn,
assumeRoleExternalId: destination.assumeRoleExternalId,
assumeRoleSessionTags: destination.assumeRoleSessionTags,
assumeRoleAdditionalOptions:
destination.assumeRoleAdditionalOptions as AWS.STS.Types.AssumeRoleRequest,
region: destination.region,
quiet: options.quiet,
});
Expand All @@ -129,7 +130,7 @@ export class ContainerImageAssetHandler implements IAssetHandler {
destinationAlreadyExists: await this.destinationAlreadyExists(ecr, destination, imageUri),
};

return this.init;
return this.init!;
}

/**
Expand Down
6 changes: 3 additions & 3 deletions lib/private/handlers/files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export class FileAssetHandler implements IAssetHandler {
const s3 = await this.host.aws.s3Client({
assumeRoleArn: destination.assumeRoleArn,
assumeRoleExternalId: destination.assumeRoleExternalId,
assumeRoleSessionTags: destination.assumeRoleSessionTags,
assumeRoleAdditionalOptions: destination.assumeRoleAdditionalOptions,
region: destination.region,
quiet: true,
});
Expand All @@ -59,7 +59,7 @@ export class FileAssetHandler implements IAssetHandler {
const s3 = await this.host.aws.s3Client({
assumeRoleArn: destination.assumeRoleArn,
assumeRoleExternalId: destination.assumeRoleExternalId,
assumeRoleSessionTags: destination.assumeRoleSessionTags,
assumeRoleAdditionalOptions: destination.assumeRoleAdditionalOptions,
region: destination.region,
});
this.host.emitMessage(EventType.CHECK, `Check ${s3Url}`);
Expand All @@ -73,7 +73,7 @@ export class FileAssetHandler implements IAssetHandler {
await this.host.aws.discoverTargetAccount({
assumeRoleArn: destination.assumeRoleArn,
assumeRoleExternalId: destination.assumeRoleExternalId,
assumeRoleSessionTags: destination.assumeRoleSessionTags,
assumeRoleAdditionalOptions: destination.assumeRoleAdditionalOptions,
region: destination.region,
})
)?.accountId;
Expand Down
117 changes: 117 additions & 0 deletions test/aws.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import * as os from 'os';
import { DefaultAwsClient } from '../lib';

afterEach(() => {
jest.requireActual('aws-sdk');
});

beforeEach(() => {
jest.requireActual('aws-sdk');
});

test('assumeRole passes the right parameters to STS', async () => {
// eslint-disable-next-line @typescript-eslint/no-require-imports
const AWS = require('aws-sdk');

jest.mock('aws-sdk', () => {
return {
STS: jest.fn().mockReturnValue({
getCallerIdentity: jest.fn().mockReturnValue({
promise: jest.fn().mockResolvedValue({
Account: '123456789012',
Arn: 'arn:aws:iam::123456789012:role/my-role',
}),
}),
}),
ChainableTemporaryCredentials: jest.fn(),
};
});
const aws = new DefaultAwsClient();
await withMocked(os, 'userInfo', async (userInfo) => {
userInfo.mockReturnValue({
username: 'foo',
uid: 1,
gid: 1,
homedir: '/here',
shell: '/bin/sh',
});
await aws.discoverTargetAccount({
region: 'us-east-1',
assumeRoleArn: 'arn:aws:iam::123456789012:role/my-role',
assumeRoleExternalId: 'external-id',
assumeRoleAdditionalOptions: {
DurationSeconds: 3600,
},
});
expect(AWS.ChainableTemporaryCredentials).toHaveBeenCalledWith({
params: {
ExternalId: 'external-id',
RoleArn: 'arn:aws:iam::123456789012:role/my-role',
DurationSeconds: 3600,
RoleSessionName: `cdk-assets-foo`,
},
stsConfig: {
customUserAgent: 'cdk-assets',
region: 'us-east-1',
},
});
});
});

test('assumeRole defaults session tags to all', async () => {
// eslint-disable-next-line @typescript-eslint/no-require-imports
require('aws-sdk');

jest.mock('aws-sdk', () => {
return {
STS: jest.fn().mockReturnValue({
getCallerIdentity: jest.fn().mockReturnValue({
promise: jest.fn().mockResolvedValue({
Account: '123456789012',
Arn: 'arn:aws:iam::123456789012:role/my-role',
}),
}),
}),
ChainableTemporaryCredentials: jest.fn(),
};
});

const aws = new DefaultAwsClient();

const account = await aws.discoverTargetAccount({});
expect(account).toEqual({
accountId: '123456789012',
partition: 'aws',
});
});

export function withMocked<A extends object, K extends keyof A, B>(
obj: A,
key: K,
block: (fn: jest.Mocked<A>[K]) => B
): B {
const original = obj[key];
const mockFn = jest.fn();
(obj as any)[key] = mockFn;

let asyncFinally: boolean = false;
try {
const ret = block(mockFn as any);
if (!isPromise(ret)) {
return ret;
}

asyncFinally = true;
return ret.finally(() => {
obj[key] = original;
}) as any;
} finally {
if (!asyncFinally) {
obj[key] = original;
}
}
}

function isPromise<A>(object: any): object is Promise<A> {
return Promise.resolve(object) === object;
}
8 changes: 8 additions & 0 deletions test/docker-images.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ beforeEach(() => {
theDestination: {
region: 'us-north-50',
assumeRoleArn: 'arn:aws:role',
assumeRoleExternalId: 'external-id',
assumeRoleAdditionalOptions: {
Tags: [{ Key: 'Departement', Value: 'Engineering' }],
},
repositoryName: 'repo',
imageTag: 'abcdef',
},
Expand Down Expand Up @@ -249,6 +253,10 @@ test('pass destination properties to AWS client', async () => {
expect.objectContaining({
region: 'us-north-50',
assumeRoleArn: 'arn:aws:role',
assumeRoleExternalId: 'external-id',
assumeRoleAdditionalOptions: {
Tags: [{ Key: 'Departement', Value: 'Engineering' }],
},
})
);
});
Expand Down
12 changes: 10 additions & 2 deletions test/files.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
jest.mock('child_process');

import { Manifest } from '@aws-cdk/cloud-assembly-schema';
import { FileDestination, Manifest } from '@aws-cdk/cloud-assembly-schema';
import * as mockfs from 'mock-fs';
import { FakeListener } from './fake-listener';
import { mockAws, mockedApiFailure, mockedApiResult, mockUpload } from './mock-aws';
Expand All @@ -9,9 +9,13 @@ import { AssetPublishing, AssetManifest } from '../lib';

const ABS_PATH = '/simple/cdk.out/some_external_file';

const DEFAULT_DESTINATION = {
const DEFAULT_DESTINATION: FileDestination = {
region: 'us-north-50',
assumeRoleArn: 'arn:aws:role',
assumeRoleExternalId: 'external-id',
assumeRoleAdditionalOptions: {
Tags: [{ Key: 'Departement', Value: 'Engineering' }],
},
bucketName: 'some_bucket',
objectKey: 'some_key',
};
Expand Down Expand Up @@ -114,6 +118,10 @@ test('pass destination properties to AWS client', async () => {
expect.objectContaining({
region: 'us-north-50',
assumeRoleArn: 'arn:aws:role',
assumeRoleExternalId: 'external-id',
assumeRoleAdditionalOptions: {
Tags: [{ Key: 'Departement', Value: 'Engineering' }],
},
})
);
});
Expand Down

0 comments on commit bb4f087

Please sign in to comment.