Skip to content

Commit b7cfeae

Browse files
authored
refactor(sdkv3): migrate IAM client to v3 (#6698)
## Problem Iam uses v2. ## Solution - migrate it. - rename from `DefaultIamClient` -> `IamClient`. - add stronger type assertions on request to avoid weak type assertions later on. (replace `as` with proper type checks when possible) - Continue pattern of wrapping sdk types in "safe" types to avoid assertions. --- - Treat all work as PUBLIC. Private `feature/x` branches will not be squash-merged at release time. - Your code changes must meet the guidelines in [CONTRIBUTING.md](https://github.com/aws/aws-toolkit-vscode/blob/master/CONTRIBUTING.md#guidelines). - License: I confirm that my contribution is made under the terms of the Apache 2.0 license.
1 parent 25ccd97 commit b7cfeae

File tree

22 files changed

+741
-185
lines changed

22 files changed

+741
-185
lines changed

package-lock.json

Lines changed: 524 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/core/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -506,6 +506,7 @@
506506
"@aws-sdk/client-sso-oidc": "<3.696.0",
507507
"@aws-sdk/client-ssm": "<3.696.0",
508508
"@aws-sdk/client-ec2": "<3.696.0",
509+
"@aws-sdk/client-iam": "<3.696.0",
509510
"@aws-sdk/credential-provider-env": "<3.696.0",
510511
"@aws-sdk/credential-provider-process": "<3.696.0",
511512
"@aws-sdk/credential-provider-sso": "<3.696.0",

packages/core/src/awsService/apprunner/wizards/apprunnerCreateServiceWizard.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { AppRunnerCodeRepositoryWizard } from './codeRepositoryWizard'
1515
import { GitExtension } from '../../../shared/extensions/git'
1616
import { makeDeploymentButton } from './deploymentButton'
1717
import { createExitPrompter } from '../../../shared/ui/common/exitPrompter'
18-
import { DefaultIamClient } from '../../../shared/clients/iamClient'
18+
import { IamClient } from '../../../shared/clients/iam'
1919
import { DefaultEcrClient } from '../../../shared/clients/ecrClient'
2020
import { DefaultAppRunnerClient } from '../../../shared/clients/apprunnerClient'
2121
import { getAppRunnerCreateServiceDocUrl } from '../../../shared/extensionUtilities'
@@ -102,7 +102,7 @@ export class CreateAppRunnerServiceWizard extends Wizard<AppRunner.CreateService
102102
initState: WizardState<AppRunner.CreateServiceRequest> = {},
103103
implicitState: WizardState<AppRunner.CreateServiceRequest> = {},
104104
clients = {
105-
iam: new DefaultIamClient(region),
105+
iam: new IamClient(region),
106106
ecr: new DefaultEcrClient(region),
107107
apprunner: new DefaultAppRunnerClient(region),
108108
}

packages/core/src/awsService/apprunner/wizards/imageRepositoryWizard.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55

6-
import { AppRunner, IAM } from 'aws-sdk'
6+
import { AppRunner } from 'aws-sdk'
77
import { createCommonButtons, QuickInputButton, QuickInputToggleButton } from '../../../shared/ui/buttons'
88
import { toArrayAsync } from '../../../shared/utilities/collectionUtils'
99
import { EcrClient, EcrRepository } from '../../../shared/clients/ecrClient'
@@ -17,7 +17,7 @@ import * as nls from 'vscode-nls'
1717
import { WizardForm } from '../../../shared/wizards/wizardForm'
1818
import { createVariablesPrompter } from '../../../shared/ui/common/variablesPrompter'
1919
import { makeDeploymentButton } from './deploymentButton'
20-
import { IamClient } from '../../../shared/clients/iamClient'
20+
import { IamClient, IamRole } from '../../../shared/clients/iam'
2121
import { createRolePrompter } from '../../../shared/ui/common/roles'
2222
import { getLogger } from '../../../shared/logger/logger'
2323
import { getAppRunnerCreateServiceDocUrl, isCloud9 } from '../../../shared/extensionUtilities'
@@ -35,7 +35,7 @@ interface ImagePrompterOptions {
3535
extraButtons?: QuickInputButton<void | WizardControl>
3636
}
3737

38-
function createEcrRole(client: IamClient): Promise<IAM.Role> {
38+
function createEcrRole(client: IamClient): Promise<IamRole> {
3939
const policy = {
4040
Version: '2008-10-17',
4141
Statement: [
@@ -257,14 +257,13 @@ export class AppRunnerImageRepositoryWizard extends Wizard<AppRunner.SourceConfi
257257
constructor(ecrClient: EcrClient, iamClient: IamClient, autoDeployButton = makeDeploymentButton()) {
258258
super()
259259
const form = this.form
260-
const createAccessRolePrompter = () => {
261-
return createRolePrompter(iamClient, {
260+
const createAccessRolePrompter = () =>
261+
createRolePrompter(iamClient, {
262262
title: localize('AWS.apprunner.createService.selectRole.title', 'Select a role to pull from ECR'),
263263
helpUrl: getAppRunnerCreateServiceDocUrl(),
264264
roleFilter: (role) => (role.AssumeRolePolicyDocument ?? '').includes(appRunnerEcrEntity),
265265
createRole: createEcrRole.bind(undefined, iamClient),
266266
}).transform((resp) => resp.Arn)
267-
}
268267

269268
form.ImageRepository.applyBoundForm(createImageRepositorySubForm(ecrClient, autoDeployButton))
270269
form.AuthenticationConfiguration.AccessRoleArn.bindPrompter(createAccessRolePrompter, {

packages/core/src/awsService/ec2/model.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55
import * as vscode from 'vscode'
6-
import { IAM } from 'aws-sdk'
76
import { Ec2Selection } from './prompter'
87
import { getOrInstallCli } from '../../shared/utilities/cliUtils'
98
import { isCloud9 } from '../../shared/extensionUtilities'
@@ -18,7 +17,7 @@ import {
1817
openRemoteTerminal,
1918
promptToAddInlinePolicy,
2019
} from '../../shared/remoteSession'
21-
import { DefaultIamClient } from '../../shared/clients/iamClient'
20+
import { IamClient, IamRole } from '../../shared/clients/iam'
2221
import { ErrorInformation } from '../../shared/errors'
2322
import {
2423
sshAgentSocketVariable,
@@ -53,7 +52,7 @@ interface RemoteUser {
5352
export class Ec2Connecter implements vscode.Disposable {
5453
protected ssm: SsmClient
5554
protected ec2Client: Ec2Client
56-
protected iamClient: DefaultIamClient
55+
protected iamClient: IamClient
5756
protected sessionManager: Ec2SessionTracker
5857

5958
private policyDocumentationUri = vscode.Uri.parse(
@@ -79,8 +78,8 @@ export class Ec2Connecter implements vscode.Disposable {
7978
return new Ec2Client(this.regionCode)
8079
}
8180

82-
protected createIamSdkClient(): DefaultIamClient {
83-
return new DefaultIamClient(this.regionCode)
81+
protected createIamSdkClient(): IamClient {
82+
return new IamClient(this.regionCode)
8483
}
8584

8685
public async addActiveSession(sessionId: string, instanceId: string): Promise<void> {
@@ -95,7 +94,7 @@ export class Ec2Connecter implements vscode.Disposable {
9594
return this.sessionManager.isConnectedTo(instanceId)
9695
}
9796

98-
public async getAttachedIamRole(instanceId: string): Promise<IAM.Role | undefined> {
97+
public async getAttachedIamRole(instanceId: string): Promise<IamRole | undefined> {
9998
const IamInstanceProfile = await this.ec2Client.getAttachedIamInstanceProfile(instanceId)
10099
if (IamInstanceProfile && IamInstanceProfile.Arn) {
101100
const IamRole = await this.iamClient.getIAMRoleFromInstanceProfile(IamInstanceProfile.Arn)

packages/core/src/awsService/ecs/util.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import * as nls from 'vscode-nls'
99
const localize = nls.loadMessageBundle()
1010

1111
import { EcsClient } from '../../shared/clients/ecsClient'
12-
import { DefaultIamClient, IamClient } from '../../shared/clients/iamClient'
12+
import { IamClient } from '../../shared/clients/iam'
1313
import { ToolkitError } from '../../shared/errors'
1414
import { isCloud9 } from '../../shared/extensionUtilities'
1515
import { getOrInstallCli } from '../../shared/utilities/cliUtils'
@@ -80,7 +80,7 @@ export async function prepareCommand(
8080
try {
8181
session = (await client.executeCommand({ ...task, command })).session!
8282
} catch (execErr) {
83-
await checkPermissionsForSsm(new DefaultIamClient(globals.regionProvider.defaultRegionId), {
83+
await checkPermissionsForSsm(new IamClient(globals.regionProvider.defaultRegionId), {
8484
taskRoleArn: taskRoleArn,
8585
}).catch((permErr) => {
8686
throw ToolkitError.chain(permErr, `${execErr}`)

packages/core/src/shared/clients/clientWrapper.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,11 @@ export abstract class ClientWrapper<C extends AwsClient> implements vscode.Dispo
2828
: globals.sdkClientBuilderV3.getAwsService(args)
2929
}
3030

31-
protected async makeRequest<CommandInput extends object, Command extends AwsCommand>(
31+
protected async makeRequest<CommandInput extends object, CommandOutput extends object, Command extends AwsCommand>(
3232
command: new (o: CommandInput) => Command,
3333
commandOptions: CommandInput
34-
) {
35-
const client = this.getClient()
36-
return await client.send(new command(commandOptions))
34+
): Promise<CommandOutput> {
35+
return await this.getClient().send(new command(commandOptions))
3736
}
3837

3938
protected makePaginatedRequest<CommandInput extends object, CommandOutput extends object, Output extends object>(

packages/core/src/shared/clients/ec2.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ import {
2222
paginateDescribeIamInstanceProfileAssociations,
2323
IamInstanceProfile,
2424
GetConsoleOutputCommand,
25+
RebootInstancesCommandOutput,
26+
GetConsoleOutputCommandOutput,
2527
} from '@aws-sdk/client-ec2'
2628
import { Timeout } from '../utilities/timeoutUtils'
2729
import { showMessageWithCancel } from '../utilities/messages'
@@ -191,7 +193,7 @@ export class Ec2Client extends ClientWrapper<EC2Client> {
191193
}
192194
}
193195

194-
public async rebootInstance(instanceId: string): Promise<void> {
196+
public async rebootInstance(instanceId: string): Promise<RebootInstancesCommandOutput> {
195197
return await this.makeRequest(RebootInstancesCommand, { InstanceIds: [instanceId] })
196198
}
197199

@@ -233,7 +235,10 @@ export class Ec2Client extends ClientWrapper<EC2Client> {
233235
}
234236

235237
public async getConsoleOutput(instanceId: string, latest: boolean): Promise<SafeEc2GetConsoleOutputResult> {
236-
const response = await this.makeRequest(GetConsoleOutputCommand, { InstanceId: instanceId, Latest: latest })
238+
const response: GetConsoleOutputCommandOutput = await this.makeRequest(GetConsoleOutputCommand, {
239+
InstanceId: instanceId,
240+
Latest: latest,
241+
})
237242

238243
return {
239244
...response,
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
/*!
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import {
7+
AttachedPolicy,
8+
AttachRolePolicyCommand,
9+
AttachRolePolicyRequest,
10+
CreateRoleCommand,
11+
CreateRoleCommandOutput,
12+
CreateRoleRequest,
13+
CreateRoleResponse,
14+
EvaluationResult,
15+
GetInstanceProfileCommand,
16+
GetInstanceProfileCommandOutput,
17+
IAMClient,
18+
ListRolesRequest,
19+
paginateListAttachedRolePolicies,
20+
paginateListRoles,
21+
PutRolePolicyCommand,
22+
PutRolePolicyCommandOutput,
23+
Role,
24+
SimulatePolicyResponse,
25+
SimulatePrincipalPolicyCommand,
26+
SimulatePrincipalPolicyRequest,
27+
} from '@aws-sdk/client-iam'
28+
import { AsyncCollection } from '../utilities/asyncCollection'
29+
import { ToolkitError } from '../errors'
30+
import { ClientWrapper } from './clientWrapper'
31+
32+
export interface IamRole extends Role {
33+
RoleName: string
34+
Arn: string
35+
}
36+
37+
export interface IamCreateRoleResponse extends CreateRoleResponse {
38+
Role: IamRole
39+
}
40+
41+
export class IamClient extends ClientWrapper<IAMClient> {
42+
public constructor(public override readonly regionCode: string) {
43+
super(regionCode, IAMClient)
44+
}
45+
46+
public getRoles(request: ListRolesRequest = {}, maxPages: number = 500): AsyncCollection<IamRole[]> {
47+
return this.makePaginatedRequest(paginateListRoles, request, (p) => p.Roles)
48+
.limit(maxPages)
49+
.map((roles) => roles.filter(hasRequiredFields))
50+
}
51+
52+
/** Gets all roles. */
53+
public async resolveRoles(request: ListRolesRequest = {}): Promise<IamRole[]> {
54+
return this.getRoles(request).flatten().promise()
55+
}
56+
57+
public async createRole(request: CreateRoleRequest): Promise<IamCreateRoleResponse> {
58+
const response: CreateRoleCommandOutput = await this.makeRequest(CreateRoleCommand, request)
59+
if (!response.Role || !hasRequiredFields(response.Role)) {
60+
throw new ToolkitError('Failed to create IAM role')
61+
}
62+
return response as IamCreateRoleResponse // Safe to assume by check above.
63+
}
64+
65+
public async attachRolePolicy(request: AttachRolePolicyRequest): Promise<AttachRolePolicyCommand> {
66+
return await this.makeRequest(AttachRolePolicyCommand, request)
67+
}
68+
69+
public async simulatePrincipalPolicy(request: SimulatePrincipalPolicyRequest): Promise<SimulatePolicyResponse> {
70+
return await this.makeRequest(SimulatePrincipalPolicyCommand, request)
71+
}
72+
73+
/**
74+
* Attempts to verify if a role has the provided permissions.
75+
*/
76+
public async getDeniedActions(request: SimulatePrincipalPolicyRequest): Promise<EvaluationResult[]> {
77+
const permissionResponse = await this.simulatePrincipalPolicy(request)
78+
if (!permissionResponse.EvaluationResults) {
79+
throw new Error('No evaluation results found')
80+
}
81+
82+
// Ignore deny from Organization SCP. These can result in false negatives.
83+
// See https://github.com/aws/aws-sdk/issues/102
84+
return permissionResponse.EvaluationResults.filter(
85+
(r) => r.EvalDecision !== 'allowed' && r.OrganizationsDecisionDetail?.AllowedByOrganizations !== false
86+
)
87+
}
88+
89+
public getFriendlyName(arn: string): string {
90+
const tokens = arn.split('/')
91+
if (tokens.length < 2) {
92+
throw new Error(`Invalid IAM role ARN (expected format: arn:aws:iam::{id}/{name}): ${arn}`)
93+
}
94+
return tokens[tokens.length - 1]
95+
}
96+
97+
public listAttachedRolePolicies(arn: string): AsyncCollection<AttachedPolicy[]> {
98+
return this.makePaginatedRequest(
99+
paginateListAttachedRolePolicies,
100+
{
101+
RoleName: this.getFriendlyName(arn),
102+
},
103+
(p) => p.AttachedPolicies
104+
)
105+
}
106+
107+
public async getIAMRoleFromInstanceProfile(instanceProfileArn: string): Promise<IamRole> {
108+
const response: GetInstanceProfileCommandOutput = await this.makeRequest(GetInstanceProfileCommand, {
109+
InstanceProfileName: this.getFriendlyName(instanceProfileArn),
110+
})
111+
if (
112+
!response.InstanceProfile?.Roles ||
113+
response.InstanceProfile.Roles.length === 0 ||
114+
!hasRequiredFields(response.InstanceProfile.Roles[0])
115+
) {
116+
throw new ToolkitError(`Failed to find IAM role associated with Instance profile ${instanceProfileArn}`)
117+
}
118+
return response.InstanceProfile.Roles[0]
119+
}
120+
121+
public async putRolePolicy(
122+
roleArn: string,
123+
policyName: string,
124+
policyDocument: string
125+
): Promise<PutRolePolicyCommandOutput> {
126+
return await this.makeRequest(PutRolePolicyCommand, {
127+
RoleName: this.getFriendlyName(roleArn),
128+
PolicyName: policyName,
129+
PolicyDocument: policyDocument,
130+
})
131+
}
132+
}
133+
134+
function hasRequiredFields(role: Role): role is IamRole {
135+
return role.RoleName !== undefined && role.Arn !== undefined
136+
}

0 commit comments

Comments
 (0)