Skip to content

Commit 26dbb00

Browse files
committed
feat(auth): detect when connected to non-AWS cloud
And general fixes when connection is to non-AWS endpoints - Add helper function to identify when connected to LocalStack - Make LiveTail and DocDB read endpoint URL if exists (they don't use the generic ClientBuilder) - Disable S3 virtual-host-style and host prefix for LocalStack - Disable host prefixes for all services when using LocalStack - Send telemetry for custom endpoints and for LocalStack connections
1 parent feaa1b9 commit 26dbb00

File tree

10 files changed

+230
-29
lines changed

10 files changed

+230
-29
lines changed

packages/core/src/auth/deprecated/loginManager.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,11 @@ import { isAutomation } from '../../shared/vscode/env'
3030
import { Credentials } from '@aws-sdk/types'
3131
import { ToolkitError } from '../../shared/errors'
3232
import * as localizedText from '../../shared/localizedText'
33-
import { DefaultStsClient } from '../../shared/clients/stsClient'
33+
import { DefaultStsClient, type GetCallerIdentityResponse } from '../../shared/clients/stsClient'
3434
import { findAsync } from '../../shared/utilities/collectionUtils'
3535
import { telemetry } from '../../shared/telemetry/telemetry'
3636
import { withTelemetryContext } from '../../shared/telemetry/util'
37+
import { localStackConnectionHeader, localStackConnectionString } from '../utils'
3738

3839
const loginManagerClassName = 'LoginManager'
3940
/**
@@ -117,14 +118,34 @@ export class LoginManager {
117118
region = this.defaultCredentialsRegion
118119
) {
119120
const stsClient = new DefaultStsClient(region, credentials, endpointUrl)
120-
const accountId = (await stsClient.getCallerIdentity()).Account
121+
const callerIdentity = await stsClient.getCallerIdentity()
122+
await this.detectExternalConnection(callerIdentity)
123+
// Validate presence of Account Id
124+
const accountId = callerIdentity.Account
121125
if (!accountId) {
126+
if (endpointUrl !== undefined) {
127+
telemetry.auth_customEndpoint.emit({ source: 'validateCredentials', result: 'Failed' })
128+
}
122129
throw new Error('Could not determine Account Id for credentials')
123130
}
131+
if (endpointUrl !== undefined) {
132+
telemetry.auth_customEndpoint.emit({ source: 'validateCredentials', result: 'Succeeded' })
133+
}
124134

125135
return accountId
126136
}
127137

138+
private async detectExternalConnection(callerIdentity: GetCallerIdentityResponse): Promise<void> {
139+
// @ts-ignore
140+
const headers = callerIdentity.$response?.httpResponse?.headers
141+
if (headers !== undefined && localStackConnectionHeader in headers) {
142+
await globals.globalState.update('aws.toolkit.externalConnection', localStackConnectionString)
143+
telemetry.auth_localstackEndpoint.emit({ source: 'validateCredentials', result: 'Succeeded' })
144+
} else {
145+
await globals.globalState.update('aws.toolkit.externalConnection', undefined)
146+
}
147+
}
148+
128149
/**
129150
* Removes Credentials from the Toolkit. Essentially the Toolkit becomes "logged out".
130151
*

packages/core/src/auth/utils.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -890,3 +890,12 @@ export async function getAuthType() {
890890
}
891891
return authType
892892
}
893+
894+
export const localStackConnectionHeader = 'x-localstack'
895+
export const localStackConnectionString = 'localstack'
896+
897+
export function isLocalStackConnection(): boolean {
898+
return (
899+
globals.globalState.tryGet('aws.toolkit.externalConnection', String, undefined) === localStackConnectionString
900+
)
901+
}

packages/core/src/awsService/cloudWatchLogs/registry/liveTailSession.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import * as vscode from 'vscode'
66
import * as AWS from '@aws-sdk/types'
77
import {
88
CloudWatchLogsClient,
9+
type CloudWatchLogsClientConfig,
910
StartLiveTailCommand,
1011
StartLiveTailResponseStream,
1112
} from '@aws-sdk/client-cloudwatch-logs'
@@ -53,12 +54,17 @@ export class LiveTailSession {
5354
this._logGroupArn = configuration.logGroupArn
5455
this.logStreamFilter = configuration.logStreamFilter
5556
this.logEventFilterPattern = configuration.logEventFilterPattern
57+
const cwlClientProps: CloudWatchLogsClientConfig = {
58+
credentials: configuration.awsCredentials,
59+
region: configuration.region,
60+
customUserAgent: getUserAgent(),
61+
}
62+
const endpointUrl = globals.awsContext.getCredentialEndpointUrl()
63+
if (endpointUrl !== undefined) {
64+
cwlClientProps.endpoint = endpointUrl
65+
}
5666
this.liveTailClient = {
57-
cwlClient: new CloudWatchLogsClient({
58-
credentials: configuration.awsCredentials,
59-
region: configuration.region,
60-
customUserAgent: getUserAgent(),
61-
}),
67+
cwlClient: new CloudWatchLogsClient(cwlClientProps),
6268
abortController: new AbortController(),
6369
}
6470
this._maxLines = LiveTailSession.settings.get('limit', 10000)

packages/core/src/shared/awsClientBuilder.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { AwsContext } from './awsContext'
1010
import { DevSettings } from './settings'
1111
import { getUserAgent } from './telemetry/util'
1212
import { telemetry } from './telemetry/telemetry'
13+
import { isLocalStackConnection } from '../auth/utils'
1314

1415
/** Suppresses a very noisy warning printed by AWS SDK v2, which clutters local debugging output, CI logs, etc. */
1516
export function disableAwsSdkWarning() {
@@ -146,9 +147,13 @@ export class DefaultAWSClientBuilder implements AWSClientBuilder {
146147

147148
// Get endpoint url from the active profile if there's no endpoint directly passed as a parameter
148149
const endpointUrl = this.awsContext.getCredentialEndpointUrl()
149-
if (opt.endpoint === undefined && endpointUrl !== undefined) {
150+
if (!('endpoint' in opt) && endpointUrl !== undefined) {
150151
opt.endpoint = endpointUrl
151152
}
153+
if (isLocalStackConnection()) {
154+
// Disable host prefixes for LocalStack
155+
opt.hostPrefixEnabled = false
156+
}
152157
// Then check if there's an endpoint in the dev settings
153158
if (serviceName) {
154159
opt.endpoint = settings.get('endpoints', {})[serviceName] ?? opt.endpoint

packages/core/src/shared/awsClientBuilderV3.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {
3131
RetryStrategy,
3232
UserAgent,
3333
} from '@aws-sdk/types'
34+
import { S3Client } from '@aws-sdk/client-s3'
3435
import { FetchHttpHandler } from '@smithy/fetch-http-handler'
3536
import { HttpResponse, HttpRequest } from '@aws-sdk/protocol-http'
3637
import { ConfiguredRetryStrategy } from '@smithy/util-retry'
@@ -42,6 +43,7 @@ import { partialClone } from './utilities/collectionUtils'
4243
import { selectFrom } from './utilities/tsUtils'
4344
import { once } from './utilities/functionUtils'
4445
import { isWeb } from './extensionGlobals'
46+
import { isLocalStackConnection } from '../auth/utils'
4547

4648
export type AwsClientConstructor<C> = new (o: AwsClientOptions) => C
4749
export type AwsCommandConstructor<CommandInput extends object, Command extends AwsCommand<CommandInput, object>> = new (
@@ -81,6 +83,8 @@ export interface AwsClientOptions {
8183
retryStrategy: RetryStrategy | RetryStrategyV2
8284
logger: Logger
8385
token: TokenIdentity | TokenIdentityProvider
86+
forcePathStyle: boolean
87+
hostPrefixEnabled: boolean
8488
}
8589

8690
interface AwsServiceOptions<C extends AwsClient> {
@@ -176,9 +180,20 @@ export class AWSClientBuilderV3 {
176180
}
177181
// Get endpoint url from the active profile if there's no endpoint directly passed as a parameter
178182
const endpointUrl = this.context.getCredentialEndpointUrl()
179-
if (opt.endpoint === undefined && endpointUrl !== undefined) {
183+
if (!('endpoint' in opt) && endpointUrl !== undefined) {
184+
// Because we check that 'endpoint' doesn't exist in `opt`, TS complains when we actually add it
185+
// @ts-expect-error TS2339
180186
opt.endpoint = endpointUrl
181187
}
188+
if (isLocalStackConnection()) {
189+
// Disable host prefixes for LocalStack
190+
opt.hostPrefixEnabled = false
191+
// serviceClient name gets minified, but it's always consistent
192+
if (serviceOptions.serviceClient.name === S3Client.name) {
193+
// Use path-style S3 URLs for LocalStack
194+
opt.forcePathStyle = true
195+
}
196+
}
182197
const service = new serviceOptions.serviceClient(opt)
183198
service.middlewareStack.add(telemetryMiddleware, { step: 'deserialize' })
184199
service.middlewareStack.add(loggingMiddleware, { step: 'finalizeRequest' })

packages/core/src/shared/clients/docdbClient.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,16 @@ export class DefaultDocumentDBClient {
3737

3838
private async getSdkConfig() {
3939
const credentials = await globals.awsContext.getCredentials()
40-
return {
40+
const endpointUrl = globals.awsContext.getCredentialEndpointUrl()
41+
const config = {
4142
customUserAgent: getUserAgent({ includePlatform: true, includeClientId: true }),
4243
credentials: credentials,
4344
region: this.regionCode,
4445
}
46+
if (endpointUrl !== undefined) {
47+
return { ...config, endpoint: endpointUrl }
48+
}
49+
return config
4550
}
4651

4752
public async getClient(): Promise<DocDB.DocDBClient> {

packages/core/src/shared/clients/stsClient.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { Credentials } from '@aws-sdk/types'
88
import globals from '../extensionGlobals'
99
import { ClassToInterfaceType } from '../utilities/tsUtils'
1010

11+
export type GetCallerIdentityResponse = STS.GetCallerIdentityResponse
1112
export type StsClient = ClassToInterfaceType<DefaultStsClient>
1213
export class DefaultStsClient {
1314
public constructor(

packages/core/src/shared/globalState.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@ export type globalKey =
8383
| 'aws.lambda.remoteDebugSnapshot'
8484
// List of Domain-Users to show/hide Sagemaker SpaceApps in AWS Explorer.
8585
| 'aws.sagemaker.selectedDomainUsers'
86+
// Name of the connection if it's not to the AWS cloud. Current supported value only 'localstack'
87+
| 'aws.toolkit.externalConnection'
8688

8789
/**
8890
* Extension-local (not visible to other vscode extensions) shared state which persists after IDE

packages/core/src/shared/telemetry/vscodeTelemetry.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1191,6 +1191,14 @@
11911191
"name": "appbuilder_lambda2sam",
11921192
"description": "User click Convert a lambda function to SAM project"
11931193
},
1194+
{
1195+
"name": "auth_customEndpoint",
1196+
"description": "User used a custom endpoint"
1197+
},
1198+
{
1199+
"name": "auth_localstackEndpoint",
1200+
"description": "User used a LocalStack connection"
1201+
},
11941202
{
11951203
"name": "lambda_remoteDebugStop",
11961204
"description": "user stop remote debugging",

0 commit comments

Comments
 (0)