Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: additional options when assuming publishing roles #40

Merged
merged 34 commits into from
Sep 19, 2024
Merged
Show file tree
Hide file tree
Changes from 33 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
591220f
chore: create release branches for v2 and v3
TheRealAmazonKendra Aug 5, 2024
bf4baf3
chore(deps): upgrade dependencies (#4)
aws-cdk-automation Aug 6, 2024
37b9c05
chore(deps): upgrade dependencies (#6)
aws-cdk-automation Aug 7, 2024
0471c78
chore(deps): upgrade dependencies (#8)
aws-cdk-automation Aug 8, 2024
858b890
chore(deps): upgrade dependencies (#10)
aws-cdk-automation Aug 9, 2024
d3ebf87
chore(deps): upgrade dependencies (#12)
aws-cdk-automation Aug 10, 2024
8193654
chore(deps): upgrade dependencies (#14)
aws-cdk-automation Aug 11, 2024
22c788e
chore(deps): upgrade dependencies (#16)
aws-cdk-automation Aug 13, 2024
ef81c90
chore(deps): upgrade dependencies (#19)
aws-cdk-automation Aug 13, 2024
f4629fd
chore(deps): upgrade dependencies (#21)
aws-cdk-automation Aug 15, 2024
bffb501
chore(deps): upgrade dependencies (#23)
aws-cdk-automation Aug 16, 2024
44047f8
chore(deps): upgrade dependencies (#25)
aws-cdk-automation Aug 17, 2024
bc6b26c
chore(deps): upgrade dependencies (#27)
aws-cdk-automation Aug 18, 2024
7e83fd8
chore(deps): upgrade dependencies (#30)
aws-cdk-automation Aug 20, 2024
27c2071
chore(deps): upgrade dependencies (#32)
aws-cdk-automation Aug 21, 2024
ecd13a1
chore(deps): upgrade dependencies (#34)
aws-cdk-automation Aug 22, 2024
e32e763
chore(deps): upgrade dependencies (#36)
aws-cdk-automation Aug 23, 2024
3051c9c
chore(deps): upgrade dependencies (#39)
aws-cdk-automation Aug 24, 2024
2260990
feat: add session tags to ClientOptions interface
sumupitchayan Aug 24, 2024
02892e2
add session tags to assumeRole function
sumupitchayan Aug 24, 2024
276ef63
better typing
iliapolo Aug 29, 2024
9ba3fd1
bring back type for session tags
iliapolo Aug 29, 2024
bb4f087
mid work
iliapolo Aug 31, 2024
911260e
mid work
iliapolo Aug 31, 2024
d39577a
Merge branch 'v2-main' into sumughan/add-session-tags-to-client-options
iliapolo Aug 31, 2024
b81a4c6
mid work
iliapolo Aug 31, 2024
211e526
mid work
iliapolo Sep 1, 2024
2e79a94
mid work
iliapolo Sep 1, 2024
d1281af
mid work
iliapolo Sep 3, 2024
fbc510a
mid work
iliapolo Sep 18, 2024
32a8f24
Merge branch 'v2-main' into sumughan/add-session-tags-to-client-options
iliapolo Sep 18, 2024
5c1ea4f
chore: self mutation
invalid-email-address Sep 18, 2024
61c5a38
mid work
iliapolo Sep 18, 2024
b406ba1
Merge branch 'v2-main' into sumughan/add-session-tags-to-client-options
iliapolo Sep 19, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .projen/deps.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions .projen/tasks.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions .projenrc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const project = new typescript.TypeScriptProject({
'@types/glob',
'@types/mime',
'@types/yargs',
'@types/mock-fs',
iliapolo marked this conversation as resolved.
Show resolved Hide resolved
'fs-extra',
'graceful-fs',
'jszip',
Expand Down
17 changes: 15 additions & 2 deletions lib/aws.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +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;
assumeRoleAdditionalOptions?: AssumeRoleAdditionalOptions;
quiet?: boolean;
}

Expand Down Expand Up @@ -118,7 +125,8 @@ export class DefaultAwsClient implements IAws {
credentials = await this.assumeRole(
options.region,
options.assumeRoleArn,
options.assumeRoleExternalId
options.assumeRoleExternalId,
options.assumeRoleAdditionalOptions
);
}

Expand All @@ -140,13 +148,18 @@ export class DefaultAwsClient implements IAws {
private async assumeRole(
region: string | undefined,
roleArn: string,
externalId?: string
externalId?: string,
additionalOptions?: AssumeRoleAdditionalOptions
): Promise<AWS.Credentials> {
return new this.AWS.ChainableTemporaryCredentials({
params: {
RoleArn: roleArn,
ExternalId: externalId,
RoleSessionName: `cdk-assets-${safeUsername()}`,
TransitiveTagKeys: additionalOptions?.Tags
? additionalOptions.Tags.map((t) => t.Key)
: undefined,
...(additionalOptions ?? {}),
},
stsConfig: {
region,
Expand Down
3 changes: 2 additions & 1 deletion lib/private/handlers/container-images.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as path from 'path';
import { DockerImageDestination } from '@aws-cdk/cloud-assembly-schema';
import type * as AWS from 'aws-sdk';
import { destinationToClientOptions } from '.';
import { DockerImageManifestEntry } from '../../asset-manifest';
import { EventType } from '../../progress';
import { IAssetHandler, IHandlerHost, IHandlerOptions } from '../asset-handler';
Expand Down Expand Up @@ -105,7 +106,7 @@ export class ContainerImageAssetHandler implements IAssetHandler {

const destination = await replaceAwsPlaceholders(this.asset.destination, this.host.aws);
const ecr = await this.host.aws.ecrClient({
...destination,
...destinationToClientOptions(destination),
quiet: options.quiet,
});
const account = async () => (await this.host.aws.discoverCurrentAccount())?.accountId;
Expand Down
10 changes: 7 additions & 3 deletions lib/private/handlers/files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { createReadStream, promises as fs } from 'fs';
import * as path from 'path';
import { FileAssetPackaging, FileSource } from '@aws-cdk/cloud-assembly-schema';
import * as mime from 'mime';
import { destinationToClientOptions } from '.';
import { FileManifestEntry } from '../../asset-manifest';
import { EventType } from '../../progress';
import { zipDirectory } from '../archive';
Expand Down Expand Up @@ -35,7 +36,7 @@ export class FileAssetHandler implements IAssetHandler {
const s3Url = `s3://${destination.bucketName}/${destination.objectKey}`;
try {
const s3 = await this.host.aws.s3Client({
...destination,
...destinationToClientOptions(destination),
quiet: true,
});
this.host.emitMessage(EventType.CHECK, `Check ${s3Url}`);
Expand All @@ -53,14 +54,17 @@ export class FileAssetHandler implements IAssetHandler {
public async publish(): Promise<void> {
const destination = await replaceAwsPlaceholders(this.asset.destination, this.host.aws);
const s3Url = `s3://${destination.bucketName}/${destination.objectKey}`;
const s3 = await this.host.aws.s3Client(destination);

const clientOptions = destinationToClientOptions(destination);
const s3 = await this.host.aws.s3Client(clientOptions);
this.host.emitMessage(EventType.CHECK, `Check ${s3Url}`);

const bucketInfo = BucketInformation.for(this.host);

// A thunk for describing the current account. Used when we need to format an error
// message, not in the success case.
const account = async () => (await this.host.aws.discoverTargetAccount(destination))?.accountId;
const account = async () =>
(await this.host.aws.discoverTargetAccount(clientOptions))?.accountId;
switch (await bucketInfo.bucketOwnership(s3, destination.bucketName)) {
case BucketOwnership.MINE:
break;
Expand Down
14 changes: 14 additions & 0 deletions lib/private/handlers/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { AwsDestination } from '@aws-cdk/cloud-assembly-schema';
import { ContainerImageAssetHandler } from './container-images';
import { FileAssetHandler } from './files';
import {
Expand All @@ -6,6 +7,7 @@ import {
FileManifestEntry,
IManifestEntry,
} from '../../asset-manifest';
import type { ClientOptions } from '../../aws';
import { IAssetHandler, IHandlerHost, IHandlerOptions } from '../asset-handler';

export function makeAssetHandler(
Expand All @@ -23,3 +25,15 @@ export function makeAssetHandler(

throw new Error(`Unrecognized asset type: '${asset}'`);
}

export function destinationToClientOptions(destination: AwsDestination): ClientOptions {
// Explicitly build ClientOptions from AwsDestination. The fact they are structurally compatible is coincidental.
// This also enforces better type checking that cdk-assets depends on the appropriate version of
// @aws-cdk/cloud-assembly-schema.
return {
assumeRoleArn: destination.assumeRoleArn,
assumeRoleExternalId: destination.assumeRoleExternalId,
assumeRoleAdditionalOptions: destination.assumeRoleAdditionalOptions,
region: destination.region,
};
}
3 changes: 2 additions & 1 deletion package.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

136 changes: 136 additions & 0 deletions test/aws.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import * as os from 'os';
import { DefaultAwsClient } from '../lib';

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
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: {
Tags: [{ Key: 'Departement', Value: 'Engineering' }],
},
});
expect(AWS.ChainableTemporaryCredentials).toHaveBeenCalledWith({
params: {
ExternalId: 'external-id',
RoleArn: 'arn:aws:iam::123456789012:role/my-role',
Tags: [{ Key: 'Departement', Value: 'Engineering' }],
TransitiveTagKeys: ['Departement'],
RoleSessionName: `cdk-assets-foo`,
},
stsConfig: {
customUserAgent: 'cdk-assets',
region: 'us-east-1',
},
});
});
});

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
Loading