Skip to content

Commit 5bab70a

Browse files
authWebview: Can connect with credentials (#3448)
Ability to enter credentials from auth webview - Validation of inputs - No duplicate profile name - Checks credentials can be authenticated before submission - Connects to credentials upon submission Signed-off-by: Nikolas Komonen <[email protected]>
1 parent a43ed0b commit 5bab70a

23 files changed

+867
-83
lines changed

resources/css/base.css

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,13 +55,15 @@ body.vscode-light input[type='checkbox']:checked {
5555

5656
/* Text/number input box */
5757
input[type='text'],
58+
input[type='password'],
5859
input[type='number'] {
5960
color: var(--vscode-settings-textInputForeground);
6061
background: var(--vscode-settings-textInputBackground);
6162
border: 1px solid var(--vscode-settings-textInputBorder);
6263
padding: 4px 4px;
6364
}
6465
input[type='text'][data-invalid='true'],
66+
input[type='password'][data-invalid='true'],
6567
input[type='number'][data-invalid='true'] {
6668
border: 1px solid var(--vscode-inputValidation-errorBorder);
6769
border-bottom: 0;

src/credentials/auth.ts

Lines changed: 73 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,14 @@ import { Commands } from '../shared/vscode/commands2'
1818
import { createQuickPick, DataQuickPickItem, showQuickPick } from '../shared/ui/pickerPrompter'
1919
import { isValidResponse } from '../shared/wizards/wizard'
2020
import { CancellationError, Timeout } from '../shared/utilities/timeoutUtils'
21-
import { errorCode, formatError, ToolkitError, UnknownError } from '../shared/errors'
21+
import { errorCode, formatError, isAwsError, ToolkitError, UnknownError } from '../shared/errors'
2222
import { getCache } from './sso/cache'
2323
import { createFactoryFunction, isNonNullable, Mutable } from '../shared/utilities/tsUtils'
2424
import { builderIdStartUrl, SsoToken } from './sso/model'
2525
import { SsoClient } from './sso/clients'
2626
import { getLogger } from '../shared/logger'
2727
import { CredentialsProviderManager } from './providers/credentialsProviderManager'
28-
import { asString, CredentialsProvider, fromString } from './providers/credentials'
28+
import { asString, CredentialsId, CredentialsProvider, fromString } from './providers/credentials'
2929
import { once } from '../shared/utilities/functionUtils'
3030
import { getResourceFromTreeNode } from '../shared/treeview/utils'
3131
import { Instance } from '../shared/utilities/typeConstructors'
@@ -46,8 +46,9 @@ import { AsyncCollection, toCollection } from '../shared/utilities/asyncCollecti
4646
import { join, toStream } from '../shared/utilities/collectionUtils'
4747
import { getConfigFilename } from './sharedCredentialsFile'
4848
import { saveProfileToCredentials } from './sharedCredentials'
49-
import { SectionName, StaticCredentialsProfileKeys } from './types'
49+
import { SectionName, SharedCredentialsKeys, StaticProfile, StaticProfileKeyErrorMessage } from './types'
5050
import { throwOnInvalidCredentials } from './sharedCredentialsValidation'
51+
import { TempCredentialProvider } from './providers/tempCredentialsProvider'
5152

5253
export const ssoScope = 'sso:account:access'
5354
export const codecatalystScopes = ['codecatalyst:read_write']
@@ -586,6 +587,54 @@ export class Auth implements AuthService, ConnectionManager {
586587
return this.#validationErrors.get(connection.id)
587588
}
588589

590+
/**
591+
* Authenticates the given data and returns error info if it fails.
592+
*
593+
* @returns undefined if authentication succeeds, otherwise object with error info
594+
*/
595+
public async authenticateData(data: StaticProfile): Promise<StaticProfileKeyErrorMessage | undefined> {
596+
const tempId = await this.addTempCredential(data)
597+
const tempIdString = asString(tempId)
598+
try {
599+
await this.reauthenticate({ id: tempIdString })
600+
} catch (e) {
601+
if (isAwsError(e)) {
602+
if (e.code === 'InvalidClientTokenId') {
603+
return { key: SharedCredentialsKeys.AWS_ACCESS_KEY_ID, error: 'Invalid access key' }
604+
} else if (e.code === 'SignatureDoesNotMatch') {
605+
return { key: SharedCredentialsKeys.AWS_SECRET_ACCESS_KEY, error: 'Invalid secret key' }
606+
}
607+
}
608+
throw e
609+
} finally {
610+
await this.removeTempCredential(tempId)
611+
}
612+
return undefined
613+
}
614+
615+
private async addTempCredential(data: StaticProfile): Promise<CredentialsId> {
616+
const tempProvider = new TempCredentialProvider(data)
617+
this.iamProfileProvider.addProvider(tempProvider)
618+
await this.thrownOnConn(tempProvider.getCredentialsId(), 'not-exists')
619+
return tempProvider.getCredentialsId()
620+
}
621+
private async removeTempCredential(id: CredentialsId) {
622+
this.iamProfileProvider.removeProvider(id)
623+
await this.thrownOnConn(id, 'exists')
624+
}
625+
626+
private async thrownOnConn(id: CredentialsId, throwOn: 'exists' | 'not-exists') {
627+
const idAsString = asString(id)
628+
const conns = await this.listConnections() // triggers loading of profile in to store
629+
const connExists = conns.some(conn => conn.id === idAsString)
630+
631+
if (throwOn === 'exists' && connExists) {
632+
throw new ToolkitError(`Conn should not exist: ${idAsString}`)
633+
} else if (throwOn === 'not-exists' && !connExists) {
634+
throw new ToolkitError(`Conn should exist: ${idAsString}`)
635+
}
636+
}
637+
589638
/**
590639
* Attempts to remove all auth state related to the connection.
591640
*
@@ -1204,16 +1253,33 @@ const addConnection = Commands.register({ id: 'aws.auth.addConnection', telemetr
12041253

12051254
export async function tryAddCredentials(
12061255
profileName: SectionName,
1207-
profileData: StaticCredentialsProfileKeys,
1256+
profileData: StaticProfile,
12081257
tryConnect = true
12091258
): Promise<boolean> {
1259+
const auth = Auth.instance
1260+
1261+
// sanity checks
12101262
await throwOnInvalidCredentials(profileName, profileData)
1263+
const authenticationError = await auth.authenticateData(profileData)
1264+
if (authenticationError) {
1265+
throw new ToolkitError(`Found error with '${authenticationError.key}':'${authenticationError.error}' `, {
1266+
code: 'InvalidCredentials',
1267+
})
1268+
}
1269+
12111270
await saveProfileToCredentials(profileName, profileData)
1271+
12121272
if (tryConnect) {
1213-
const auth = Auth.instance
1214-
const conn = await auth.getConnection({ id: profileName })
1273+
const id = asString({
1274+
credentialSource: 'profile',
1275+
credentialTypeId: profileName,
1276+
})
1277+
const conn = await auth.getConnection({ id })
1278+
12151279
if (conn === undefined) {
1216-
throw new ToolkitError('Failed to get connection from profile', { code: 'MissingConnection' })
1280+
throw new ToolkitError(`Failed to get connection from profile: ${profileName}`, {
1281+
code: 'MissingConnection',
1282+
})
12171283
}
12181284

12191285
await auth.useConnection(conn)

src/credentials/providers/credentials.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,11 +67,12 @@ export function isEqual(idA: CredentialsId, idB: CredentialsId): boolean {
6767
* - "sso" refers to all SSO-based profiles. Currently, any profile with a start URL will
6868
* be treated as SSO _by the Toolkit_. Incomplete profiles may be rejected by the SDKs, so
6969
* valid SSO profiles may not necessarily be considered valid among all tools.
70-
*
70+
* - "temp" refers to credentials that are used temporarily within the code.
71+
* This can be for something like testing raw credentials data
7172
* Compare the similar concept `telemetry.CredentialSourceId`.
7273
*/
73-
export type CredentialsProviderType = typeof credentialsProviderType[number]
74-
export const credentialsProviderType = ['profile', 'ec2', 'ecs', 'env', 'sso'] as const
74+
export type CredentialsProviderType = (typeof credentialsProviderType)[number]
75+
export const credentialsProviderType = ['profile', 'ec2', 'ecs', 'env', 'sso', 'temp'] as const
7576

7677
/**
7778
* Lossy map of CredentialsProviderType to telemetry.CredentialSourceId
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/*!
2+
* Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import { Credentials } from '@aws-sdk/types'
7+
import { CredentialType } from '../../shared/telemetry/telemetry.gen'
8+
import { getStringHash } from '../../shared/utilities/textUtilities'
9+
import { CredentialsId, CredentialsProvider, CredentialsProviderType } from './credentials'
10+
import { StaticProfile } from '../types'
11+
import { Auth } from '../auth'
12+
13+
/**
14+
* HACK: A credentials provider for a temporary use case.
15+
*
16+
* This provides the bare minimum way to use credentials
17+
* in the rest of the system.
18+
*
19+
* It is currently only used in {@link Auth} and is hidden
20+
* within the class.
21+
*/
22+
export class TempCredentialProvider implements CredentialsProvider {
23+
private credentials: StaticProfile
24+
constructor(data: StaticProfile) {
25+
this.credentials = {
26+
aws_access_key_id: data.aws_access_key_id,
27+
aws_secret_access_key: data.aws_secret_access_key,
28+
}
29+
}
30+
31+
public async isAvailable(): Promise<boolean> {
32+
return true
33+
}
34+
35+
public getCredentialsId(): CredentialsId {
36+
return {
37+
credentialSource: this.getProviderType(),
38+
credentialTypeId: this.getHashCode(),
39+
}
40+
}
41+
42+
public static getProviderType(): CredentialsProviderType {
43+
return 'temp'
44+
}
45+
46+
public getProviderType(): CredentialsProviderType {
47+
return TempCredentialProvider.getProviderType()
48+
}
49+
50+
public getTelemetryType(): CredentialType {
51+
return 'other'
52+
}
53+
54+
public getHashCode(): string {
55+
return getStringHash(JSON.stringify(this.credentials))
56+
}
57+
58+
public getDefaultRegion(): string | undefined {
59+
return undefined
60+
}
61+
62+
public async canAutoConnect(): Promise<boolean> {
63+
return false
64+
}
65+
66+
public async getCredentials(): Promise<Credentials> {
67+
return {
68+
accessKeyId: this.credentials.aws_access_key_id,
69+
secretAccessKey: this.credentials.aws_secret_access_key,
70+
}
71+
}
72+
}

src/credentials/sharedCredentials.ts

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { SystemUtilities } from '../shared/systemUtilities'
88
import { ToolkitError } from '../shared/errors'
99
import { assertHasProps } from '../shared/utilities/tsUtils'
1010
import { getConfigFilename, getCredentialsFilename } from './sharedCredentialsFile'
11-
import { SectionName, StaticCredentialsProfileKeys } from './types'
11+
import { SectionName, StaticProfile } from './types'
1212
import { UserCredentialsUtils } from '../shared/credentials/userCredentialsUtils'
1313

1414
export async function updateAwsSdkLoadConfigEnvVar(): Promise<void> {
@@ -124,9 +124,9 @@ function validateSection(section: BaseSection): asserts section is Section {
124124
*/
125125
export async function loadSharedCredentialsProfiles(): Promise<Record<SectionName, Profile>> {
126126
const profiles = {} as Record<SectionName, Profile>
127-
for (const [k, v] of (await loadSharedCredentialsSections()).sections.entries()) {
128-
if (v.type === 'profile') {
129-
profiles[k] = extractDataFromSection(v)
127+
for (const section of (await loadSharedCredentialsSections()).sections.values()) {
128+
if (section.type === 'profile') {
129+
profiles[section.name] = extractDataFromSection(section)
130130
}
131131
}
132132
return profiles
@@ -239,10 +239,7 @@ async function loadCredentialsFile(credentialsUri?: vscode.Uri): Promise<ReturnT
239239
/**
240240
* Saves the given profile data to the credentials file.
241241
*/
242-
export async function saveProfileToCredentials(
243-
profileName: SectionName,
244-
profileData: StaticCredentialsProfileKeys
245-
): Promise<void> {
242+
export async function saveProfileToCredentials(profileName: SectionName, profileData: StaticProfile): Promise<void> {
246243
if (await profileExists(profileName)) {
247244
throw new ToolkitError(`Cannot save profile "${profileName}" because it already exists.`, {
248245
code: 'ProfileAlreadyExists',

src/credentials/sharedCredentialsValidation.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
* This module focuses on the validation of shared credentials properties
66
*/
77

8-
import { localize } from 'vscode-nls'
8+
import * as nls from 'vscode-nls'
9+
const localize = nls.loadMessageBundle()
910
import { CredentialsData, CredentialsKey, SectionName, SharedCredentialsKeys } from './types'
1011
import { ToolkitError } from '../shared/errors'
1112
import { profileExists } from './sharedCredentials'
@@ -41,7 +42,9 @@ export function getCredentialsErrors(
4142
}
4243
errors[key] = validateFunc(key, value)
4344
})
44-
if (Object.keys(errors).length === 0) {
45+
46+
const hasErrors = Object.values(errors).some(v => v)
47+
if (!hasErrors) {
4548
return
4649
}
4750
return errors

src/credentials/types.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,11 @@ export type CredentialsKey = (typeof SharedCredentialsKeys)[keyof typeof SharedC
3434
*
3535
* https://docs.aws.amazon.com/sdkref/latest/guide/feature-static-credentials.html
3636
*/
37-
export type StaticCredentialsProfileKeysOptional = Pick<CredentialsData, 'aws_access_key_id' | 'aws_secret_access_key'>
38-
export type StaticCredentialsProfileKeys = Required<StaticCredentialsProfileKeysOptional>
37+
export type StaticProfileOptional = Pick<CredentialsData, 'aws_access_key_id' | 'aws_secret_access_key'>
38+
export type StaticProfile = Required<StaticProfileOptional>
39+
export type StaticProfileKey = keyof StaticProfile
40+
/** An error for a specific static profile key */
41+
export type StaticProfileKeyErrorMessage = { key: StaticProfileKey; error: string }
3942

4043
/**
4144
* The name of a section in a credentials/config file

src/credentials/vue/ServiceItemContent.vue

Lines changed: 0 additions & 29 deletions
This file was deleted.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Auth Forms
2+
3+
These are the components which the user will interact with and enter the necessary
4+
auth data.
5+
6+
## General Design
7+
8+
Each auth form component utilizes an underlying state class which keeps
9+
track of the data. The state class can then be used elsewhere to retrieve
10+
the latest information about that auth method, these class instances can
11+
be found in [shared.vue](./shared.vue).
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<script lang="ts">
2+
import { defineComponent } from 'vue'
3+
4+
export default defineComponent({
5+
emits: ['auth-connection-updated'],
6+
methods: {
7+
emitAuthConnectionUpdated(id: AuthFormId) {
8+
this.$emit('auth-connection-updated', id)
9+
},
10+
},
11+
})
12+
13+
export interface AuthStatus {
14+
/**
15+
* Returns true if the auth is successfully connected.
16+
*/
17+
isAuthConnected(): Promise<boolean>
18+
}
19+
20+
export class UnimplementedAuthStatus implements AuthStatus {
21+
isAuthConnected(): Promise<boolean> {
22+
return Promise.resolve(false)
23+
}
24+
}
25+
26+
export const authForms = {
27+
CREDENTIALS: 'CREDENTIALS',
28+
} as const
29+
30+
export type AuthFormId = (typeof authForms)[keyof typeof authForms]
31+
</script>

0 commit comments

Comments
 (0)