Skip to content

Commit

Permalink
deps(auth): remove dependence on deprecated and outdated @aws-sdk/*
Browse files Browse the repository at this point in the history
… packages. (aws#6474)

The auth code relies on old versions of `@aws-sdk/*` that have since
been deprecated or are no longer backward compatible, making versions
bumps impossible.
- `@aws-sdk/credential-provider-imds` has since been
[deprecated](https://www.npmjs.com/package/@aws-sdk/credential-provider-imds)
- `fromIni` from `@aws-sdk/credential-provider-ini` no longer supports
passing a `loadedConfig`.
- `AssumeRoleParams` is no longer exported by
`@aws-sdk/credential-provider-ini`.

We need to be able to bump these `@aws-sdk/*` package versions to
continue to consume newer generated clients. Being pinned to older
versions is also a security risk. See
aws#6439 for more
information.

- write custom credentials provider to replace `fromIni` with
`loadedConfig` option.
- drop dependency on `@aws-sdk/credential-provider-ini` since its no
longer used.
- add direct dependency on `@aws-sdk/credential-provider-env` since this
was installed as part of `@aws-sdk-credential-provider-ini` before.
- Fix many (not all) of the deprecation warnings in auth code related to
credentials provider.

Before, we used `fromIni` with the `loadedConfig` option which allows us
to avoid reading the config file from disk on each credentials fetch and
allows us to merge the current credentials with those found in the
`.ini` file. To achieve the same behavior without the `loadedConfig`
option, we need to write our own credentials provider that supports MFA
and role assumption, and returns the desired merged credentials, rather
than reading from disk.

- Manually verify this role assumption works by following the steps
[here](https://docs.aws.amazon.com/sdkref/latest/guide/access-assume-role.html).
- Manually verify MFA works via adapting
[this](https://docs.aws.amazon.com/cli/v1/userguide/cli-configure-role.html#:~:text=This%20policy%20allows%20the%20user,they%20authenticate%20by%20using%20MFA.&text=Next%2C%20add%20a%20line%20to,by%20the%20role's%20trust%20policy.&text=The%20mfa_serial%20setting%20can%20take,command%20with%20this%20profile%20fails.&text=The%20second%20profile%20entry%2C%20role,%22:%20%5B%20%7B%20...).
(Used DuoMobile)
- Add unit tests with API calls stubbed.

- There are two tests that can now be re-enabled because of this version
bump, undoing
aws@db27ebb
- The steps to test role assumption could become an integ/e2e test.
Right now requires setting many resources up in console, but perhaps
this can all be done by the SDKs with an account on admin access.

---

- 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 and s7ab059789 committed Feb 19, 2025
1 parent ab7aa86 commit 0e53435
Show file tree
Hide file tree
Showing 10 changed files with 20,176 additions and 12,203 deletions.
4 changes: 4 additions & 0 deletions docs/faq-credentials.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,7 @@ Issue [aws-toolkit-vscode#3667](https://github.com/aws/aws-toolkit-vscode/issues
2. Attempt to sign in again with AWS Builder ID
3. If sign is is successful you can remove the old folder: `rm -rf ~/.aws/sso-OLD`
1. Or revert the change: `mv ~/.aws/sso-OLD ~/.aws/sso`

### AWS Shared Credentials File

When authenticating with IAM credentials, the profile name, access key, and secret key will be stored on disk at a default location of `~/.aws/credentials` on Linux and MacOS, and `%USERPROFILE%\.aws\credentials` on Windows machines. The toolkit also supports editting this file manually, with the format specified [here](https://docs.aws.amazon.com/sdkref/latest/guide/file-format.html#file-format-creds). The credentials files also supports [role assumption](https://docs.aws.amazon.com/sdkref/latest/guide/access-assume-role.html) and [MFA](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_mfa.html). Note that this credentials file is shared between all local AWS development tools. For more information, see the full documentation [here](https://docs.aws.amazon.com/sdkref/latest/guide/file-format.html).
32,058 changes: 19,964 additions & 12,094 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -504,7 +504,7 @@
"@aws-sdk/client-lambda": "^3.637.0",
"@aws-sdk/client-sso": "^3.342.0",
"@aws-sdk/client-sso-oidc": "^3.574.0",
"@aws-sdk/credential-provider-ini": "3.46.0",
"@aws-sdk/credential-provider-env": "3.696.0",
"@aws-sdk/credential-provider-process": "3.37.0",
"@aws-sdk/credential-provider-sso": "^3.345.0",
"@aws-sdk/property-provider": "3.46.0",
Expand Down
83 changes: 83 additions & 0 deletions packages/core/scripts/build/copyFiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@
/* eslint-disable no-restricted-imports */
import fs from 'fs'
import * as path from 'path'
import { createInstallQNode, createLearnMoreNode, createDismissNode } from 'src/amazonq/explorer/amazonQChildrenNodes'
import { undefined } from 'src/amazonq/explorer/amazonQTreeNode'
import { AuthState } from 'src/codewhisperer'
import { Commands } from 'src/shared'
import { TreeNode, ResourceTreeDataProvider } from 'src/shared/treeview/resourceTreeDataProvider'
/*!
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
import * as vscode from 'vscode'

// Moves all dependencies into `dist`

Expand Down Expand Up @@ -71,3 +81,76 @@ function main() {
}

void main()
export class AmazonQNode implements TreeNode {
public readonly id = 'amazonq';
public readonly resource = this;
private readonly onDidChangeChildrenEmitter = new vscode.EventEmitter<void>();
private readonly onDidChangeTreeItemEmitter = new vscode.EventEmitter<void>();
public readonly onDidChangeTreeItem = this.onDidChangeTreeItemEmitter.event;
public readonly onDidChangeChildren = this.onDidChangeChildrenEmitter.event;
private readonly onDidChangeVisibilityEmitter = new vscode.EventEmitter<void>();
public readonly onDidChangeVisibility = this.onDidChangeVisibilityEmitter.event;

public static amazonQState: AuthState

private constructor() { }

public getTreeItem() {
const item = new vscode.TreeItem('Amazon Q')
item.collapsibleState = vscode.TreeItemCollapsibleState.Collapsed
item.contextValue = 'awsAmazonQNode'

return item
}

public refresh(): void {
this.onDidChangeChildrenEmitter.fire()
}

public refreshRootNode() {
this.onDidChangeTreeItemEmitter.fire()
}

public getChildren() {
const children = [createInstallQNode(), createLearnMoreNode(), createDismissNode()]
return children
}

/**
* HACK: Since this is assumed to be an immediate child of the
* root, we return undefined.
*
* TODO: Look to have a base root class to extend so we do not
* need to implement this here.
* @returns
*/
getParent(): TreeNode<unknown> | undefined {
return undefined
}

static #instance: AmazonQNode

static get instance(): AmazonQNode {
return (this.#instance ??= new AmazonQNode())
}
}
/**
* Refreshes the Amazon Q Tree node. If Amazon Q's connection state is provided, it will also internally
* update the connection state.
*
* This command is meant to be called by Amazon Q. It doesn't serve much purpose being called otherwise.
*/

export const refreshAmazonQ = (provider?: ResourceTreeDataProvider) => Commands.register({ id: '_aws.toolkit.amazonq.refreshTreeNode', logging: false }, () => {
AmazonQNode.instance.refresh()
if (provider) {
provider.refresh()
}
})

export const refreshAmazonQRootNode = (provider?: ResourceTreeDataProvider) => Commands.register({ id: '_aws.amazonq.refreshRootNode', logging: false }, () => {
AmazonQNode.instance.refreshRootNode()
if (provider) {
provider.refresh()
}
})
80 changes: 0 additions & 80 deletions packages/core/src/amazonq/explorer/amazonQTreeNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,84 +3,4 @@
* SPDX-License-Identifier: Apache-2.0
*/

import * as vscode from 'vscode'
import { ResourceTreeDataProvider, TreeNode } from '../../shared/treeview/resourceTreeDataProvider'
import { AuthState } from '../../codewhisperer/util/authUtil'
import { createLearnMoreNode, createInstallQNode, createDismissNode } from './amazonQChildrenNodes'
import { Commands } from '../../shared/vscode/commands2'

export class AmazonQNode implements TreeNode {
public readonly id = 'amazonq'
public readonly resource = this
private readonly onDidChangeChildrenEmitter = new vscode.EventEmitter<void>()
private readonly onDidChangeTreeItemEmitter = new vscode.EventEmitter<void>()
public readonly onDidChangeTreeItem = this.onDidChangeTreeItemEmitter.event
public readonly onDidChangeChildren = this.onDidChangeChildrenEmitter.event
private readonly onDidChangeVisibilityEmitter = new vscode.EventEmitter<void>()
public readonly onDidChangeVisibility = this.onDidChangeVisibilityEmitter.event

public static amazonQState: AuthState

private constructor() {}

public getTreeItem() {
const item = new vscode.TreeItem('Amazon Q')
item.collapsibleState = vscode.TreeItemCollapsibleState.Collapsed
item.contextValue = 'awsAmazonQNode'

return item
}

public refresh(): void {
this.onDidChangeChildrenEmitter.fire()
}

public refreshRootNode() {
this.onDidChangeTreeItemEmitter.fire()
}

public getChildren() {
const children = [createInstallQNode(), createLearnMoreNode(), createDismissNode()]
return children
}

/**
* HACK: Since this is assumed to be an immediate child of the
* root, we return undefined.
*
* TODO: Look to have a base root class to extend so we do not
* need to implement this here.
* @returns
*/
getParent(): TreeNode<unknown> | undefined {
return undefined
}

static #instance: AmazonQNode

static get instance(): AmazonQNode {
return (this.#instance ??= new AmazonQNode())
}
}

/**
* Refreshes the Amazon Q Tree node. If Amazon Q's connection state is provided, it will also internally
* update the connection state.
*
* This command is meant to be called by Amazon Q. It doesn't serve much purpose being called otherwise.
*/
export const refreshAmazonQ = (provider?: ResourceTreeDataProvider) =>
Commands.register({ id: '_aws.toolkit.amazonq.refreshTreeNode', logging: false }, () => {
AmazonQNode.instance.refresh()
if (provider) {
provider.refresh()
}
})

export const refreshAmazonQRootNode = (provider?: ResourceTreeDataProvider) =>
Commands.register({ id: '_aws.amazonq.refreshRootNode', logging: false }, () => {
AmazonQNode.instance.refreshRootNode()
if (provider) {
provider.refresh()
}
})
2 changes: 1 addition & 1 deletion packages/core/src/auth/providers/ec2CredentialsProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*/

import { Credentials } from '@aws-sdk/types'
import { fromInstanceMetadata } from '@aws-sdk/credential-provider-imds'
import { fromInstanceMetadata } from '@smithy/credential-provider-imds'
import { DefaultEc2MetadataClient } from '../../shared/clients/ec2MetadataClient'
import { Ec2MetadataClient } from '../../shared/clients/ec2MetadataClient'
import { getLogger } from '../../shared/logger/logger'
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/auth/providers/ecsCredentialsProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*/

import { Credentials, CredentialProvider } from '@aws-sdk/types'
import { fromContainerMetadata } from '@aws-sdk/credential-provider-imds'
import { fromContainerMetadata } from '@smithy/credential-provider-imds'
import { EnvironmentVariables } from '../../shared/environmentVariables'
import { CredentialType } from '../../shared/telemetry/telemetry.gen'
import { getStringHash } from '../../shared/utilities/textUtilities'
Expand Down
73 changes: 48 additions & 25 deletions packages/core/src/auth/providers/sharedCredentialsProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,10 @@
*/

import * as AWS from '@aws-sdk/types'
import { AssumeRoleParams, fromIni } from '@aws-sdk/credential-provider-ini'
import { fromProcess } from '@aws-sdk/credential-provider-process'
import { ParsedIniData, SharedConfigFiles } from '@smithy/shared-ini-file-loader'
import { ParsedIniData } from '@smithy/types'
import { chain } from '@aws-sdk/property-provider'
import { fromInstanceMetadata, fromContainerMetadata } from '@aws-sdk/credential-provider-imds'
import { fromInstanceMetadata, fromContainerMetadata } from '@smithy/credential-provider-imds'
import { fromEnv } from '@aws-sdk/credential-provider-env'
import { getLogger } from '../../shared/logger/logger'
import { getStringHash } from '../../shared/utilities/textUtilities'
Expand All @@ -29,9 +28,10 @@ import {
Profile,
Section,
} from '../credentials/sharedCredentials'
import { SectionName, SharedCredentialsKeys } from '../credentials/types'
import { CredentialsData, SectionName, SharedCredentialsKeys } from '../credentials/types'
import { SsoProfile, hasScopes, scopesSsoAccountAccess } from '../connection'
import { builderIdStartUrl } from '../sso/constants'
import { ToolkitError } from '../../shared/errors'

const credentialSources = {
ECS_CONTAINER: 'EcsContainer',
Expand Down Expand Up @@ -378,18 +378,6 @@ export class SharedCredentialsProvider implements CredentialsProvider {
}

private makeSharedIniFileCredentialsProvider(loadedCreds?: ParsedIniData): AWS.CredentialProvider {
const assumeRole = async (credentials: AWS.Credentials, params: AssumeRoleParams) => {
const region = this.getDefaultRegion() ?? 'us-east-1'
const stsClient = new DefaultStsClient(region, credentials)
const response = await stsClient.assumeRole(params)
return {
accessKeyId: response.Credentials!.AccessKeyId!,
secretAccessKey: response.Credentials!.SecretAccessKey!,
sessionToken: response.Credentials?.SessionToken,
expiration: response.Credentials?.Expiration,
}
}

// Our credentials logic merges profiles from the credentials and config files but SDK v3 does not
// This can cause odd behavior where the Toolkit can switch to a profile but not authenticate with it
// So the workaround is to do give the SDK the merged profiles directly
Expand All @@ -399,15 +387,50 @@ export class SharedCredentialsProvider implements CredentialsProvider {
(k) => this.getProfile(k)
)

return fromIni({
profile: this.profileName,
mfaCodeProvider: async (mfaSerial) => await getMfaTokenFromUser(mfaSerial, this.profileName),
roleAssumer: assumeRole,
loadedConfig: Promise.resolve({
credentialsFile: loadedCreds ?? profiles,
configFile: {},
} as SharedConfigFiles),
})
return async () => {
const iniData = loadedCreds ?? profiles
const profile: CredentialsData = iniData[this.profileName]
if (!profile) {
throw new ToolkitError(`auth: Profile ${this.profileName} not found`)
}
// No role to assume, return static credentials.
if (!profile.role_arn) {
return {
accessKeyId: profile.aws_access_key_id!,
secretAccessKey: profile.aws_secret_access_key!,
sessionToken: profile.aws_session_token,
}
}
if (!profile.source_profile || !iniData[profile.source_profile]) {
throw new ToolkitError(
`auth: Profile ${this.profileName} is missing source_profile for role assumption`
)
}
// Use source profile to assume IAM role based on role ARN provided.
const sourceProfile = iniData[profile.source_profile!]
const stsClient = new DefaultStsClient(this.getDefaultRegion() ?? 'us-east-1', {
accessKeyId: sourceProfile.aws_access_key_id!,
secretAccessKey: sourceProfile.aws_secret_access_key!,
})
// Prompt for MFA Token if needed.
const assumeRoleReq = {
RoleArn: profile.role_arn,
RoleSessionName: 'AssumeRoleSession',
...(profile.mfa_serial
? {
SerialNumber: profile.mfa_serial,
TokenCode: await getMfaTokenFromUser(profile.mfa_serial, this.profileName),
}
: {}),
}
const assumeRoleRsp = await stsClient.assumeRole(assumeRoleReq)
return {
accessKeyId: assumeRoleRsp.Credentials!.AccessKeyId!,
secretAccessKey: assumeRoleRsp.Credentials!.SecretAccessKey!,
sessionToken: assumeRoleRsp.Credentials?.SessionToken,
expiration: assumeRoleRsp.Credentials?.Expiration,
}
}
}

private makeSourcedCredentialsProvider(): AWS.CredentialProvider {
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/awsexplorer/activation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import { CdkRootNode } from '../awsService/cdk/explorer/rootNode'
import { CodeCatalystRootNode } from '../codecatalyst/explorer'
import { CodeCatalystAuthenticationProvider } from '../codecatalyst/auth'
import { S3FolderNode } from '../awsService/s3/explorer/s3FolderNode'
import { AmazonQNode, refreshAmazonQ, refreshAmazonQRootNode } from '../amazonq/explorer/amazonQTreeNode'
import { AmazonQNode, refreshAmazonQ, refreshAmazonQRootNode } from 'scripts/build/copyFiles'
import { activateViewsShared, registerToolView } from './activationShared'
import { isExtensionInstalled } from '../shared/utilities/vsCodeUtils'
import { CommonAuthViewProvider } from '../login/webview/commonAuthViewProvider'
Expand Down
Loading

0 comments on commit 0e53435

Please sign in to comment.