Skip to content

Commit e4cf912

Browse files
Merge master into feature/emr
2 parents 4e9725d + 42da514 commit e4cf912

File tree

3 files changed

+77
-10
lines changed

3 files changed

+77
-10
lines changed

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

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,14 @@ 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'
2324
import { sshAgentSocketVariable, startSshAgent, startVscodeRemote } from '../../shared/extensions/ssh'
2425
import { createBoundProcess } from '../../codecatalyst/model'
2526
import { getLogger } from '../../shared/logger/logger'
26-
import { Timeout } from '../../shared/utilities/timeoutUtils'
27+
import { CancellationError, Timeout } from '../../shared/utilities/timeoutUtils'
2728
import { showMessageWithCancel } from '../../shared/utilities/messages'
2829
import { SshConfig, sshLogFileLocation } from '../../shared/sshConfig'
2930
import { SshKeyPair } from './sshKeyPair'
@@ -113,13 +114,11 @@ export class Ec2ConnectionManager {
113114
const hasPermission = await this.hasProperPermissions(IamRole!.Arn)
114115

115116
if (!hasPermission) {
116-
const message = `Ensure an IAM role with the required policies 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+
throw new CancellationError('user')
121+
}
123122
}
124123
}
125124

packages/core/src/shared/remoteSession.ts

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import * as nls from 'vscode-nls'
88
const localize = nls.loadMessageBundle()
99

1010
import { Settings } from '../shared/settings'
11-
import { showMessageWithCancel } from './utilities/messages'
11+
import { showConfirmationMessage, showMessageWithCancel } from './utilities/messages'
1212
import { CancellationError, Timeout } from './utilities/timeoutUtils'
1313
import { isExtensionInstalled, showInstallExtensionMsg } from './utilities/vsCodeUtils'
1414
import { VSCODE_EXTENSION_ID, vscodeExtensionMinVersion } from './extensions'
@@ -21,6 +21,9 @@ import { ChildProcess } from './utilities/childProcess'
2121
import { findSshPath, getVscodeCliPath } from './utilities/pathFind'
2222
import { IamClient } from './clients/iamClient'
2323
import { IAM } from 'aws-sdk'
24+
import { getIdeProperties } from './extensionUtilities'
25+
26+
const policyAttachDelay = 5000
2427

2528
export interface MissingTool {
2629
readonly name: 'code' | 'ssm' | 'ssh'
@@ -32,6 +35,9 @@ const minimumSsmActions = [
3235
'ssmmessages:CreateDataChannel',
3336
'ssmmessages:OpenControlChannel',
3437
'ssmmessages:OpenDataChannel',
38+
'ssm:DescribeAssociation',
39+
'ssm:ListAssociations',
40+
'ssm:UpdateInstanceInformation',
3541
]
3642

3743
export async function openRemoteTerminal(options: vscode.TerminalOptions, onClose: () => void) {
@@ -175,6 +181,68 @@ export async function handleMissingTool(tools: Err<MissingTool[]>) {
175181
)
176182
}
177183

184+
function getFormattedSsmActions() {
185+
const formattedActions = minimumSsmActions.map((action) => `"${action}",\n`).reduce((l, r) => l + r)
186+
187+
return formattedActions.slice(0, formattedActions.length - 2)
188+
}
189+
190+
/**
191+
* Shows a progress message for adding inline policy to the role, then adds the policy.
192+
* Importantly, it keeps the progress bar up for `policyAttachDelay` additional ms to allow permissions to propagate.
193+
* If user cancels, it throws a CancellationError and stops the process from subsequently opening a connection.
194+
* @param client IamClient to be use to add the permissions.
195+
* @param roleArn Arn of the role the inline policy should be added to.
196+
*/
197+
async function addInlinePolicyWithDelay(client: IamClient, roleArn: string) {
198+
const timeout = new Timeout(policyAttachDelay)
199+
const message = `Adding Inline Policy to ${roleArn}`
200+
await showMessageWithCancel(message, timeout)
201+
await addSsmActionsToInlinePolicy(client, roleArn)
202+
203+
function delay(ms: number) {
204+
return new Promise((resolve) => setTimeout(resolve, ms))
205+
}
206+
207+
await delay(policyAttachDelay)
208+
if (timeout.elapsedTime < policyAttachDelay) {
209+
throw new CancellationError('user')
210+
}
211+
timeout.cancel()
212+
}
213+
214+
export async function promptToAddInlinePolicy(client: IamClient, roleArn: string): Promise<boolean> {
215+
const promptText = `${
216+
getIdeProperties().company
217+
} Toolkit will add required actions to role ${roleArn}:\n${getFormattedSsmActions()}`
218+
const confirmation = await showConfirmationMessage({ prompt: promptText, confirm: 'Approve' })
219+
220+
if (confirmation) {
221+
await addInlinePolicyWithDelay(client, roleArn)
222+
}
223+
224+
return confirmation
225+
}
226+
227+
async function addSsmActionsToInlinePolicy(client: IamClient, roleArn: string) {
228+
const policyName = 'AWSVSCodeRemoteConnect'
229+
const policyDocument = getSsmPolicyDocument()
230+
await client.putRolePolicy(roleArn, policyName, policyDocument)
231+
}
232+
233+
function getSsmPolicyDocument() {
234+
return `{
235+
"Version": "2012-10-17",
236+
"Statement": {
237+
"Effect": "Allow",
238+
"Action": [
239+
${getFormattedSsmActions()}
240+
],
241+
"Resource": "*"
242+
}
243+
}`
244+
}
245+
178246
export async function getDeniedSsmActions(client: IamClient, roleArn: string): Promise<IAM.EvaluationResult[]> {
179247
const deniedActions = await client.getDeniedActions({
180248
PolicySourceArn: roleArn,

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ describe('Ec2ConnectClient', function () {
4444

4545
describe('hasProperPermissions', async function () {
4646
it('throws error when sdk throws error', async function () {
47-
sinon.stub(DefaultIamClient.prototype, 'listAttachedRolePolicies').throws(new ToolkitError('error'))
47+
sinon.stub(DefaultIamClient.prototype, 'simulatePrincipalPolicy').throws(new ToolkitError('error'))
4848

4949
try {
5050
await client.hasProperPermissions('')

0 commit comments

Comments
 (0)