Skip to content

Commit c3b0ae9

Browse files
committed
Merge branch 'hkobew/ec2/addInlinePolicies' into feature/ec2
2 parents d266c54 + a8af781 commit c3b0ae9

File tree

3 files changed

+91
-18
lines changed

3 files changed

+91
-18
lines changed

src/ec2/model.ts

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
ensureDependencies,
1818
getDeniedSsmActions,
1919
openRemoteTerminal,
20+
promptToAddInlinePolicy,
2021
} from '../shared/remoteSession'
2122
import { DefaultIamClient } from '../shared/clients/iamClient'
2223
import { ErrorInformation } from '../shared/errors'
@@ -113,19 +114,22 @@ export class Ec2ConnectionManager {
113114
const hasPermission = await this.hasProperPermissions(IamRole!.Arn)
114115

115116
if (!hasPermission) {
116-
const message = `Ensure an IAM role with the proper permissions is attached to the instance. Found attached role: ${
117-
IamRole!.Arn
118-
}`
119-
this.throwConnectionError(message, selection, {
120-
code: 'EC2SSMPermission',
121-
documentationUri: this.policyDocumentationUri,
122-
})
117+
const policiesAdded = await promptToAddInlinePolicy(this.iamClient, IamRole!.Arn!)
118+
119+
if (!policiesAdded) {
120+
const message = `Did not add permissions. Ensure an IAM role with the proper permissions is attached to the instance. Found attached role: ${
121+
IamRole!.Arn
122+
}`
123+
this.throwConnectionError(message, selection, {
124+
code: 'EC2SSMPermission',
125+
documentationUri: this.policyDocumentationUri,
126+
})
127+
}
123128
}
124129
}
125130

126131
private async checkForInstanceSsmError(selection: Ec2Selection): Promise<void> {
127132
const isSsmAgentRunning = (await this.ssmClient.getInstanceAgentPingStatus(selection.instanceId)) == 'Online'
128-
129133
if (!isSsmAgentRunning) {
130134
this.throwConnectionError('Is SSM Agent running on the target instance?', selection, {
131135
code: 'EC2SSMAgentStatus',
@@ -135,7 +139,7 @@ export class Ec2ConnectionManager {
135139
}
136140

137141
public throwGeneralConnectionError(selection: Ec2Selection, error: Error) {
138-
this.throwConnectionError('Unable to connect to target instance. ', selection, {
142+
this.throwConnectionError('', selection, {
139143
code: 'EC2SSMConnect',
140144
cause: error,
141145
})

src/shared/clients/iamClient.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,4 +104,12 @@ export class DefaultIamClient {
104104
}
105105
return response.InstanceProfile.Roles[0]
106106
}
107+
108+
public async putRolePolicy(roleArn: string, policyName: string, policyDocument: string): Promise<void> {
109+
const client = await this.createSdkClient()
110+
const roleName = this.getFriendlyName(roleArn)
111+
await client
112+
.putRolePolicy({ RoleName: roleName, PolicyName: policyName, PolicyDocument: policyDocument })
113+
.promise()
114+
}
107115
}

src/shared/remoteSession.ts

Lines changed: 70 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,9 @@
55

66
import * as vscode from 'vscode'
77
import * as nls from 'vscode-nls'
8-
const localize = nls.loadMessageBundle()
98

109
import { Settings } from '../shared/settings'
11-
import { showMessageWithCancel } from './utilities/messages'
10+
import { showConfirmationMessage, showMessageWithCancel } from './utilities/messages'
1211
import { CancellationError, Timeout } from './utilities/timeoutUtils'
1312
import { isExtensionInstalled, showInstallExtensionMsg } from './utilities/vsCodeUtils'
1413
import { VSCODE_EXTENSION_ID, vscodeExtensionMinVersion } from './extensions'
@@ -21,16 +20,28 @@ import { pushIf } from './utilities/collectionUtils'
2120
import { ChildProcess } from './utilities/childProcess'
2221
import { IamClient } from './clients/iamClient'
2322
import { IAM } from 'aws-sdk'
23+
import { getIdeProperties } from './extensionUtilities'
24+
25+
const localize = nls.loadMessageBundle()
2426

2527
export interface MissingTool {
2628
readonly name: 'code' | 'ssm' | 'ssh'
2729
readonly reason?: string
2830
}
2931

32+
const minimumSsmActions = [
33+
'ssmmessages:CreateControlChannel',
34+
'ssmmessages:CreateDataChannel',
35+
'ssmmessages:OpenControlChannel',
36+
'ssmmessages:OpenDataChannel',
37+
]
38+
39+
const policyAttachDelay = 5000
40+
3041
export async function openRemoteTerminal(options: vscode.TerminalOptions, onClose: () => void) {
3142
const timeout = new Timeout(60000)
3243

33-
await showMessageWithCancel('AWS: Starting session...', timeout, 1000)
44+
await showMessageWithCancel('AWS: Opening remote terminal...', timeout, 1000)
3445
await withoutShellIntegration(async () => {
3546
const terminal = vscode.window.createTerminal(options)
3647

@@ -173,13 +184,63 @@ export async function handleMissingTool(tools: Err<MissingTool[]>) {
173184
export async function getDeniedSsmActions(client: IamClient, roleArn: string): Promise<IAM.EvaluationResult[]> {
174185
const deniedActions = await client.getDeniedActions({
175186
PolicySourceArn: roleArn,
176-
ActionNames: [
177-
'ssmmessages:CreateControlChannel',
178-
'ssmmessages:CreateDataChannel',
179-
'ssmmessages:OpenControlChannel',
180-
'ssmmessages:OpenDataChannel',
181-
],
187+
ActionNames: minimumSsmActions,
182188
})
183189

184190
return deniedActions
185191
}
192+
193+
export async function promptToAddInlinePolicy(client: IamClient, roleArn: string): Promise<boolean> {
194+
const promptText = `${
195+
getIdeProperties().company
196+
} Toolkit will add required actions to role ${roleArn}:\n${getFormattedSsmActions()}`
197+
const confirmation = await showConfirmationMessage({ prompt: promptText, confirm: 'Approve' })
198+
199+
if (confirmation) {
200+
await addInlinePolicyWithDelay(client, roleArn)
201+
}
202+
203+
return confirmation
204+
}
205+
206+
async function addInlinePolicyWithDelay(client: IamClient, roleArn: string) {
207+
const timeout = new Timeout(policyAttachDelay)
208+
const message = `Adding Inline Policy to ${roleArn}`
209+
await showMessageWithCancel(message, timeout)
210+
await addSsmActionsToInlinePolicy(client, roleArn)
211+
212+
function delay(ms: number) {
213+
return new Promise(resolve => setTimeout(resolve, ms))
214+
}
215+
216+
await delay(policyAttachDelay)
217+
if (timeout.elapsedTime < policyAttachDelay) {
218+
throw new CancellationError('user')
219+
}
220+
timeout.cancel()
221+
}
222+
223+
function getFormattedSsmActions() {
224+
const formattedActions = minimumSsmActions.map(action => `"${action}",\n`).reduce((l, r) => l + r)
225+
226+
return formattedActions.slice(0, formattedActions.length - 2)
227+
}
228+
229+
function getSsmPolicyDocument() {
230+
return `{
231+
"Version": "2012-10-17",
232+
"Statement": {
233+
"Effect": "Allow",
234+
"Action": [
235+
${getFormattedSsmActions()}
236+
],
237+
"Resource": "*"
238+
}
239+
}`
240+
}
241+
242+
async function addSsmActionsToInlinePolicy(client: IamClient, roleArn: string) {
243+
const policyName = 'AWSVSCodeRemoteConnect'
244+
const policyDocument = getSsmPolicyDocument()
245+
await client.putRolePolicy(roleArn, policyName, policyDocument)
246+
}

0 commit comments

Comments
 (0)