Skip to content

Commit 19043f3

Browse files
authored
fix(ec2): use IAM Role instead of instance profile #3686
* implement function to map instance profile to instance role * refactor error handling * be extra explicit that Instance Profile is different from role. * throw if no roles associated with instance profile * refactor to avoid duplicate service calls * tests: use sinon instead of fakes
1 parent ba67352 commit 19043f3

File tree

4 files changed

+161
-199
lines changed

4 files changed

+161
-199
lines changed

src/ec2/model.ts

Lines changed: 47 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { IAM } from 'aws-sdk'
88
import { Ec2Selection } from './utils'
99
import { getOrInstallCli } from '../shared/utilities/cliUtils'
1010
import { isCloud9 } from '../shared/extensionUtilities'
11-
import { ToolkitError, isAwsError } from '../shared/errors'
11+
import { ToolkitError } from '../shared/errors'
1212
import { SsmClient } from '../shared/clients/ssmClient'
1313
import { Ec2Client } from '../shared/clients/ec2Client'
1414

@@ -17,7 +17,6 @@ export type Ec2ConnectErrorCode = 'EC2SSMStatus' | 'EC2SSMPermission' | 'EC2SSMC
1717
import { openRemoteTerminal } from '../shared/remoteSession'
1818
import { DefaultIamClient } from '../shared/clients/iamClient'
1919
import { ErrorInformation } from '../shared/errors'
20-
import { getLogger } from '../shared/logger'
2120

2221
export class Ec2ConnectionManager {
2322
private ssmClient: SsmClient
@@ -28,6 +27,10 @@ export class Ec2ConnectionManager {
2827
'https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-getting-started-instance-profile.html'
2928
)
3029

30+
private ssmAgentDocumentationUri = vscode.Uri.parse(
31+
'https://docs.aws.amazon.com/systems-manager/latest/userguide/ssm-agent.html'
32+
)
33+
3134
public constructor(readonly regionCode: string) {
3235
this.ssmClient = this.createSsmSdkClient()
3336
this.ec2Client = this.createEc2SdkClient()
@@ -46,31 +49,18 @@ export class Ec2ConnectionManager {
4649
return new DefaultIamClient(this.regionCode)
4750
}
4851

49-
protected async getAttachedPolicies(instanceId: string): Promise<IAM.AttachedPolicy[]> {
50-
const IamRole = await this.ec2Client.getAttachedIamRole(instanceId)
51-
if (!IamRole?.Arn) {
52-
return []
53-
}
54-
try {
55-
const attachedPolicies = await this.iamClient.listAttachedRolePolicies(IamRole.Arn)
56-
return attachedPolicies
57-
} catch (e) {
58-
if (isAwsError(e) && e.code == 'NoSuchEntity') {
59-
const errorMessage = `Attached role does not exist in IAM: ${IamRole.Arn}.`
60-
getLogger().error(`ec2: ${errorMessage}`)
61-
throw ToolkitError.chain(e, errorMessage, {
62-
code: e.code,
63-
documentationUri: this.policyDocumentationUri,
64-
})
65-
}
66-
throw ToolkitError.chain(e as Error, `Failed to check policies for EC2 instance: ${instanceId}`, {
67-
code: 'PolicyCheck',
68-
})
52+
public async getAttachedIamRole(instanceId: string): Promise<IAM.Role | undefined> {
53+
const IamInstanceProfile = await this.ec2Client.getAttachedIamInstanceProfile(instanceId)
54+
if (IamInstanceProfile && IamInstanceProfile.Arn) {
55+
const IamRole = await this.iamClient.getIAMRoleFromInstanceProfile(IamInstanceProfile.Arn)
56+
return IamRole
6957
}
7058
}
7159

72-
public async hasProperPolicies(instanceId: string): Promise<boolean> {
73-
const attachedPolicies = (await this.getAttachedPolicies(instanceId)).map(policy => policy.PolicyName!)
60+
public async hasProperPolicies(IamRoleArn: string): Promise<boolean> {
61+
const attachedPolicies = (await this.iamClient.listAttachedRolePolicies(IamRoleArn)).map(
62+
policy => policy.PolicyName!
63+
)
7464
const requiredPolicies = ['AmazonSSMManagedInstanceCore', 'AmazonSSMManagedEC2InstanceDefaultPolicy']
7565

7666
return requiredPolicies.length !== 0 && requiredPolicies.every(policy => attachedPolicies.includes(policy))
@@ -86,46 +76,55 @@ export class Ec2ConnectionManager {
8676
throw new ToolkitError(generalErrorMessage + message, errorInfo)
8777
}
8878

89-
protected async throwPolicyError(selection: Ec2Selection) {
90-
const role = await this.ec2Client.getAttachedIamRole(selection.instanceId)
91-
92-
const baseMessage = 'Ensure an IAM role with the required policies is attached to the instance.'
93-
const messageExtension =
94-
role && role.Arn
95-
? `Found attached role ${role.Arn}.`
96-
: `Failed to find role attached to ${selection.instanceId}`
97-
const fullMessage = `${baseMessage} ${messageExtension}`
98-
99-
this.throwConnectionError(fullMessage, selection, {
100-
code: 'EC2SSMPermission',
101-
documentationUri: this.policyDocumentationUri,
102-
})
103-
}
104-
105-
public async checkForStartSessionError(selection: Ec2Selection): Promise<void> {
79+
private async checkForInstanceStatusError(selection: Ec2Selection): Promise<void> {
10680
const isInstanceRunning = await this.isInstanceRunning(selection.instanceId)
107-
const hasProperPolicies = await this.hasProperPolicies(selection.instanceId)
108-
const isSsmAgentRunning = (await this.ssmClient.getInstanceAgentPingStatus(selection.instanceId)) == 'Online'
10981

11082
if (!isInstanceRunning) {
111-
const message = 'Ensure the target instance is running and not currently starting, stopping, or stopped.'
83+
const message = 'Ensure the target instance is running.'
11284
this.throwConnectionError(message, selection, { code: 'EC2SSMStatus' })
11385
}
86+
}
87+
88+
private async checkForInstancePermissionsError(selection: Ec2Selection): Promise<void> {
89+
const IamRole = await this.getAttachedIamRole(selection.instanceId)
90+
91+
if (!IamRole) {
92+
const message = `No IAM role attached to instance: ${selection.instanceId}`
93+
this.throwConnectionError(message, selection, { code: 'EC2SSMPermission' })
94+
}
95+
96+
const hasProperPolicies = await this.hasProperPolicies(IamRole!.Arn)
11497

11598
if (!hasProperPolicies) {
116-
await this.throwPolicyError(selection)
99+
const message = `Ensure an IAM role with the required policies is attached to the instance. Found attached role: ${
100+
IamRole!.Arn
101+
}`
102+
this.throwConnectionError(message, selection, {
103+
code: 'EC2SSMPermission',
104+
documentationUri: this.policyDocumentationUri,
105+
})
117106
}
107+
}
108+
109+
private async checkForInstanceSsmError(selection: Ec2Selection): Promise<void> {
110+
const isSsmAgentRunning = (await this.ssmClient.getInstanceAgentPingStatus(selection.instanceId)) == 'Online'
118111

119112
if (!isSsmAgentRunning) {
120113
this.throwConnectionError('Is SSM Agent running on the target instance?', selection, {
121114
code: 'EC2SSMAgentStatus',
122-
documentationUri: vscode.Uri.parse(
123-
'https://docs.aws.amazon.com/systems-manager/latest/userguide/ssm-agent.html'
124-
),
115+
documentationUri: this.ssmAgentDocumentationUri,
125116
})
126117
}
127118
}
128119

120+
public async checkForStartSessionError(selection: Ec2Selection): Promise<void> {
121+
await this.checkForInstanceStatusError(selection)
122+
123+
await this.checkForInstancePermissionsError(selection)
124+
125+
await this.checkForInstanceSsmError(selection)
126+
}
127+
129128
private async openSessionInTerminal(session: Session, selection: Ec2Selection) {
130129
const ssmPlugin = await getOrInstallCli('session-manager-plugin', !isCloud9)
131130
const shellArgs = [JSON.stringify(session), selection.region, 'StartSession']

src/shared/clients/ec2Client.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -100,11 +100,11 @@ export class Ec2Client {
100100
}
101101

102102
/**
103-
* Retrieve IAM role attached to given EC2 instance.
103+
* Gets the IAM Instance Profile (not role) attached to given EC2 instance.
104104
* @param instanceId target EC2 instance ID
105-
* @returns IAM role associated with instance or undefined if none exists.
105+
* @returns IAM Instance Profile associated with instance or undefined if none exists.
106106
*/
107-
public async getAttachedIamRole(instanceId: string): Promise<IamInstanceProfile | undefined> {
107+
public async getAttachedIamInstanceProfile(instanceId: string): Promise<IamInstanceProfile | undefined> {
108108
const association = await this.getIamInstanceProfileAssociation(instanceId)
109109
return association ? association.IamInstanceProfile : undefined
110110
}

src/shared/clients/iamClient.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import globals from '../extensionGlobals'
88
import { AsyncCollection } from '../utilities/asyncCollection'
99
import { pageableToCollection } from '../utilities/collectionUtils'
1010
import { ClassToInterfaceType } from '../utilities/tsUtils'
11+
import { ToolkitError } from '../errors'
1112

1213
export type IamClient = ClassToInterfaceType<DefaultIamClient>
1314

@@ -93,4 +94,14 @@ export class DefaultIamClient {
9394

9495
return policies
9596
}
97+
98+
public async getIAMRoleFromInstanceProfile(instanceProfileArn: string): Promise<IAM.Role> {
99+
const client = await this.createSdkClient()
100+
const instanceProfileName = this.getFriendlyName(instanceProfileArn)
101+
const response = await client.getInstanceProfile({ InstanceProfileName: instanceProfileName }).promise()
102+
if (response.InstanceProfile.Roles.length === 0) {
103+
throw new ToolkitError(`Failed to find IAM role associated with Instance profile ${instanceProfileArn}`)
104+
}
105+
return response.InstanceProfile.Roles[0]
106+
}
96107
}

0 commit comments

Comments
 (0)