Skip to content

Commit

Permalink
refactor(sdkv3): migrate IAM client to v3 (#6698)
Browse files Browse the repository at this point in the history
## 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.
  • Loading branch information
Hweinstock authored Mar 3, 2025
1 parent 25ccd97 commit b7cfeae
Show file tree
Hide file tree
Showing 22 changed files with 741 additions and 185 deletions.
524 changes: 524 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,7 @@
"@aws-sdk/client-sso-oidc": "<3.696.0",
"@aws-sdk/client-ssm": "<3.696.0",
"@aws-sdk/client-ec2": "<3.696.0",
"@aws-sdk/client-iam": "<3.696.0",
"@aws-sdk/credential-provider-env": "<3.696.0",
"@aws-sdk/credential-provider-process": "<3.696.0",
"@aws-sdk/credential-provider-sso": "<3.696.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { AppRunnerCodeRepositoryWizard } from './codeRepositoryWizard'
import { GitExtension } from '../../../shared/extensions/git'
import { makeDeploymentButton } from './deploymentButton'
import { createExitPrompter } from '../../../shared/ui/common/exitPrompter'
import { DefaultIamClient } from '../../../shared/clients/iamClient'
import { IamClient } from '../../../shared/clients/iam'
import { DefaultEcrClient } from '../../../shared/clients/ecrClient'
import { DefaultAppRunnerClient } from '../../../shared/clients/apprunnerClient'
import { getAppRunnerCreateServiceDocUrl } from '../../../shared/extensionUtilities'
Expand Down Expand Up @@ -102,7 +102,7 @@ export class CreateAppRunnerServiceWizard extends Wizard<AppRunner.CreateService
initState: WizardState<AppRunner.CreateServiceRequest> = {},
implicitState: WizardState<AppRunner.CreateServiceRequest> = {},
clients = {
iam: new DefaultIamClient(region),
iam: new IamClient(region),
ecr: new DefaultEcrClient(region),
apprunner: new DefaultAppRunnerClient(region),
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* SPDX-License-Identifier: Apache-2.0
*/

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

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

form.ImageRepository.applyBoundForm(createImageRepositorySubForm(ecrClient, autoDeployButton))
form.AuthenticationConfiguration.AccessRoleArn.bindPrompter(createAccessRolePrompter, {
Expand Down
11 changes: 5 additions & 6 deletions packages/core/src/awsService/ec2/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
* SPDX-License-Identifier: Apache-2.0
*/
import * as vscode from 'vscode'
import { IAM } from 'aws-sdk'
import { Ec2Selection } from './prompter'
import { getOrInstallCli } from '../../shared/utilities/cliUtils'
import { isCloud9 } from '../../shared/extensionUtilities'
Expand All @@ -18,7 +17,7 @@ import {
openRemoteTerminal,
promptToAddInlinePolicy,
} from '../../shared/remoteSession'
import { DefaultIamClient } from '../../shared/clients/iamClient'
import { IamClient, IamRole } from '../../shared/clients/iam'
import { ErrorInformation } from '../../shared/errors'
import {
sshAgentSocketVariable,
Expand Down Expand Up @@ -53,7 +52,7 @@ interface RemoteUser {
export class Ec2Connecter implements vscode.Disposable {
protected ssm: SsmClient
protected ec2Client: Ec2Client
protected iamClient: DefaultIamClient
protected iamClient: IamClient
protected sessionManager: Ec2SessionTracker

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

protected createIamSdkClient(): DefaultIamClient {
return new DefaultIamClient(this.regionCode)
protected createIamSdkClient(): IamClient {
return new IamClient(this.regionCode)
}

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

public async getAttachedIamRole(instanceId: string): Promise<IAM.Role | undefined> {
public async getAttachedIamRole(instanceId: string): Promise<IamRole | undefined> {
const IamInstanceProfile = await this.ec2Client.getAttachedIamInstanceProfile(instanceId)
if (IamInstanceProfile && IamInstanceProfile.Arn) {
const IamRole = await this.iamClient.getIAMRoleFromInstanceProfile(IamInstanceProfile.Arn)
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/awsService/ecs/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import * as nls from 'vscode-nls'
const localize = nls.loadMessageBundle()

import { EcsClient } from '../../shared/clients/ecsClient'
import { DefaultIamClient, IamClient } from '../../shared/clients/iamClient'
import { IamClient } from '../../shared/clients/iam'
import { ToolkitError } from '../../shared/errors'
import { isCloud9 } from '../../shared/extensionUtilities'
import { getOrInstallCli } from '../../shared/utilities/cliUtils'
Expand Down Expand Up @@ -80,7 +80,7 @@ export async function prepareCommand(
try {
session = (await client.executeCommand({ ...task, command })).session!
} catch (execErr) {
await checkPermissionsForSsm(new DefaultIamClient(globals.regionProvider.defaultRegionId), {
await checkPermissionsForSsm(new IamClient(globals.regionProvider.defaultRegionId), {
taskRoleArn: taskRoleArn,
}).catch((permErr) => {
throw ToolkitError.chain(permErr, `${execErr}`)
Expand Down
7 changes: 3 additions & 4 deletions packages/core/src/shared/clients/clientWrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,11 @@ export abstract class ClientWrapper<C extends AwsClient> implements vscode.Dispo
: globals.sdkClientBuilderV3.getAwsService(args)
}

protected async makeRequest<CommandInput extends object, Command extends AwsCommand>(
protected async makeRequest<CommandInput extends object, CommandOutput extends object, Command extends AwsCommand>(
command: new (o: CommandInput) => Command,
commandOptions: CommandInput
) {
const client = this.getClient()
return await client.send(new command(commandOptions))
): Promise<CommandOutput> {
return await this.getClient().send(new command(commandOptions))
}

protected makePaginatedRequest<CommandInput extends object, CommandOutput extends object, Output extends object>(
Expand Down
9 changes: 7 additions & 2 deletions packages/core/src/shared/clients/ec2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import {
paginateDescribeIamInstanceProfileAssociations,
IamInstanceProfile,
GetConsoleOutputCommand,
RebootInstancesCommandOutput,
GetConsoleOutputCommandOutput,
} from '@aws-sdk/client-ec2'
import { Timeout } from '../utilities/timeoutUtils'
import { showMessageWithCancel } from '../utilities/messages'
Expand Down Expand Up @@ -191,7 +193,7 @@ export class Ec2Client extends ClientWrapper<EC2Client> {
}
}

public async rebootInstance(instanceId: string): Promise<void> {
public async rebootInstance(instanceId: string): Promise<RebootInstancesCommandOutput> {
return await this.makeRequest(RebootInstancesCommand, { InstanceIds: [instanceId] })
}

Expand Down Expand Up @@ -233,7 +235,10 @@ export class Ec2Client extends ClientWrapper<EC2Client> {
}

public async getConsoleOutput(instanceId: string, latest: boolean): Promise<SafeEc2GetConsoleOutputResult> {
const response = await this.makeRequest(GetConsoleOutputCommand, { InstanceId: instanceId, Latest: latest })
const response: GetConsoleOutputCommandOutput = await this.makeRequest(GetConsoleOutputCommand, {
InstanceId: instanceId,
Latest: latest,
})

return {
...response,
Expand Down
136 changes: 136 additions & 0 deletions packages/core/src/shared/clients/iam.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
/*!
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

import {
AttachedPolicy,
AttachRolePolicyCommand,
AttachRolePolicyRequest,
CreateRoleCommand,
CreateRoleCommandOutput,
CreateRoleRequest,
CreateRoleResponse,
EvaluationResult,
GetInstanceProfileCommand,
GetInstanceProfileCommandOutput,
IAMClient,
ListRolesRequest,
paginateListAttachedRolePolicies,
paginateListRoles,
PutRolePolicyCommand,
PutRolePolicyCommandOutput,
Role,
SimulatePolicyResponse,
SimulatePrincipalPolicyCommand,
SimulatePrincipalPolicyRequest,
} from '@aws-sdk/client-iam'
import { AsyncCollection } from '../utilities/asyncCollection'
import { ToolkitError } from '../errors'
import { ClientWrapper } from './clientWrapper'

export interface IamRole extends Role {
RoleName: string
Arn: string
}

export interface IamCreateRoleResponse extends CreateRoleResponse {
Role: IamRole
}

export class IamClient extends ClientWrapper<IAMClient> {
public constructor(public override readonly regionCode: string) {
super(regionCode, IAMClient)
}

public getRoles(request: ListRolesRequest = {}, maxPages: number = 500): AsyncCollection<IamRole[]> {
return this.makePaginatedRequest(paginateListRoles, request, (p) => p.Roles)
.limit(maxPages)
.map((roles) => roles.filter(hasRequiredFields))
}

/** Gets all roles. */
public async resolveRoles(request: ListRolesRequest = {}): Promise<IamRole[]> {
return this.getRoles(request).flatten().promise()
}

public async createRole(request: CreateRoleRequest): Promise<IamCreateRoleResponse> {
const response: CreateRoleCommandOutput = await this.makeRequest(CreateRoleCommand, request)
if (!response.Role || !hasRequiredFields(response.Role)) {
throw new ToolkitError('Failed to create IAM role')
}
return response as IamCreateRoleResponse // Safe to assume by check above.
}

public async attachRolePolicy(request: AttachRolePolicyRequest): Promise<AttachRolePolicyCommand> {
return await this.makeRequest(AttachRolePolicyCommand, request)
}

public async simulatePrincipalPolicy(request: SimulatePrincipalPolicyRequest): Promise<SimulatePolicyResponse> {
return await this.makeRequest(SimulatePrincipalPolicyCommand, request)
}

/**
* Attempts to verify if a role has the provided permissions.
*/
public async getDeniedActions(request: SimulatePrincipalPolicyRequest): Promise<EvaluationResult[]> {
const permissionResponse = await this.simulatePrincipalPolicy(request)
if (!permissionResponse.EvaluationResults) {
throw new Error('No evaluation results found')
}

// Ignore deny from Organization SCP. These can result in false negatives.
// See https://github.com/aws/aws-sdk/issues/102
return permissionResponse.EvaluationResults.filter(
(r) => r.EvalDecision !== 'allowed' && r.OrganizationsDecisionDetail?.AllowedByOrganizations !== false
)
}

public getFriendlyName(arn: string): string {
const tokens = arn.split('/')
if (tokens.length < 2) {
throw new Error(`Invalid IAM role ARN (expected format: arn:aws:iam::{id}/{name}): ${arn}`)
}
return tokens[tokens.length - 1]
}

public listAttachedRolePolicies(arn: string): AsyncCollection<AttachedPolicy[]> {
return this.makePaginatedRequest(
paginateListAttachedRolePolicies,
{
RoleName: this.getFriendlyName(arn),
},
(p) => p.AttachedPolicies
)
}

public async getIAMRoleFromInstanceProfile(instanceProfileArn: string): Promise<IamRole> {
const response: GetInstanceProfileCommandOutput = await this.makeRequest(GetInstanceProfileCommand, {
InstanceProfileName: this.getFriendlyName(instanceProfileArn),
})
if (
!response.InstanceProfile?.Roles ||
response.InstanceProfile.Roles.length === 0 ||
!hasRequiredFields(response.InstanceProfile.Roles[0])
) {
throw new ToolkitError(`Failed to find IAM role associated with Instance profile ${instanceProfileArn}`)
}
return response.InstanceProfile.Roles[0]
}

public async putRolePolicy(
roleArn: string,
policyName: string,
policyDocument: string
): Promise<PutRolePolicyCommandOutput> {
return await this.makeRequest(PutRolePolicyCommand, {
RoleName: this.getFriendlyName(roleArn),
PolicyName: policyName,
PolicyDocument: policyDocument,
})
}
}

function hasRequiredFields(role: Role): role is IamRole {
return role.RoleName !== undefined && role.Arn !== undefined
}
Loading

0 comments on commit b7cfeae

Please sign in to comment.