Skip to content

Commit 9936c83

Browse files
authWebview: Auth refactoring + new funcs (#3409)
- Refactors some existing auth code - Adds a new sharedCredentialsValidation class which centralizes validation of shared credentials data - Previous validation functionality was moved in to this file - Added function to be able to save credentials data (aws access key, secret key) at once to the credentials file. Signed-off-by: Nikolas Komonen <[email protected]>
1 parent 3098e5b commit 9936c83

File tree

6 files changed

+154
-24
lines changed

6 files changed

+154
-24
lines changed

src/credentials/auth.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ import { SsoCredentialsProvider } from './providers/ssoCredentialsProvider'
4545
import { AsyncCollection, toCollection } from '../shared/utilities/asyncCollection'
4646
import { join, toStream } from '../shared/utilities/collectionUtils'
4747
import { getConfigFilename } from './sharedCredentialsFile'
48+
import { saveProfileToCredentials } from './sharedCredentials'
49+
import { SectionName, StaticCredentialsProfileData } from './types'
50+
import { validateCredentialsProfile } from './sharedCredentialsValidation'
4851

4952
export const ssoScope = 'sso:account:access'
5053
export const codecatalystScopes = ['codecatalyst:read_write']
@@ -1199,6 +1202,25 @@ const addConnection = Commands.register('aws.auth.addConnection', async () => {
11991202
}
12001203
})
12011204

1205+
export async function tryAddCredentials(
1206+
profileName: SectionName,
1207+
profileData: StaticCredentialsProfileData,
1208+
tryConnect = true
1209+
): Promise<boolean> {
1210+
await validateCredentialsProfile(profileName, profileData)
1211+
await saveProfileToCredentials(profileName, profileData)
1212+
if (tryConnect) {
1213+
const auth = Auth.instance
1214+
const conn = await auth.getConnection({ id: profileName })
1215+
if (conn === undefined) {
1216+
throw new ToolkitError('Failed to get connection from profile', { code: 'MissingConnection' })
1217+
}
1218+
1219+
await auth.useConnection(conn)
1220+
}
1221+
return true
1222+
}
1223+
12021224
const getConnectionIcon = (conn: Connection) =>
12031225
conn.type === 'sso' ? getIcon('vscode-account') : getIcon('vscode-key')
12041226

src/credentials/providers/sharedCredentialsProvider.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,11 @@ import {
2828
getSectionOrThrow,
2929
isProfileSection,
3030
Profile,
31-
SectionName,
3231
Section,
3332
} from '../sharedCredentials'
3433
import { hasScopes, SsoProfile } from '../auth'
3534
import { builderIdStartUrl } from '../sso/model'
36-
import { SharedCredentialsKeys } from '../types'
35+
import { SectionName, SharedCredentialsKeys } from '../types'
3736

3837
const credentialSources = {
3938
ECS_CONTAINER: 'EcsContainer',

src/credentials/sharedCredentials.ts

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ 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, StaticCredentialsProfileData } from './types'
12+
import { UserCredentialsUtils } from '../shared/credentials/userCredentialsUtils'
1113

1214
export async function updateAwsSdkLoadConfigEnvVar(): Promise<void> {
1315
const configFileExists = await SystemUtilities.fileExists(getConfigFilename())
@@ -235,11 +237,32 @@ async function loadCredentialsFile(credentialsUri?: vscode.Uri): Promise<ReturnT
235237
}
236238

237239
/**
238-
* The name of a section in a credentials/config file
239-
*
240-
* The is the value of `{A}` in `[ {A} ]` or `[ {B} {A} ]`.
240+
* Saves the given profile data to the credentials file.
241241
*/
242-
export type SectionName = string
242+
export async function saveProfileToCredentials(
243+
profileName: SectionName,
244+
profileData: StaticCredentialsProfileData
245+
): Promise<void> {
246+
if (await profileExists(profileName)) {
247+
throw new ToolkitError(`Cannot save profile "${profileName}" because it already exists.`, {
248+
code: 'ProfileAlreadyExists',
249+
})
250+
}
251+
252+
return UserCredentialsUtils.generateCredentialsFile({
253+
profileName,
254+
accessKey: profileData.aws_access_key_id,
255+
secretKey: profileData.aws_secret_access_key,
256+
})
257+
}
258+
259+
/**
260+
* Checks if a profile exists in a shared credentials file.
261+
*/
262+
export async function profileExists(profileName: SectionName): Promise<boolean> {
263+
const existingProfiles = await loadSharedCredentialsProfiles()
264+
return Object.keys(existingProfiles).includes(profileName)
265+
}
243266

244267
export interface Profile {
245268
[key: string]: string | undefined
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/*!
2+
* Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*
5+
* This module focuses on the validation of shared credentials properties
6+
*/
7+
8+
import { localize } from 'vscode-nls'
9+
import { SectionName, SharedCredentialsKeys, StaticCredentialsProfileData } from './types'
10+
import { ToolkitError } from '../shared/errors'
11+
import { profileExists } from './sharedCredentials'
12+
13+
/**
14+
* The format validators for shared credentials keys.
15+
*
16+
* A format validator validates the format of the data,
17+
* but not the validity of the content.
18+
*/
19+
export const CredentialsKeyFormatValidators = {
20+
[SharedCredentialsKeys.AWS_ACCESS_KEY_ID]: getAccessKeyIdFormatError,
21+
[SharedCredentialsKeys.AWS_SECRET_ACCESS_KEY]: getSecretAccessKeyFormatError,
22+
} as const
23+
24+
/**
25+
* Holds the error for each key of static credentials data,
26+
* if it exists. This allows the user to get all the errors
27+
* at once.
28+
*/
29+
export type StaticCredentialsErrorResult = {
30+
[k in keyof StaticCredentialsProfileData]: string | undefined
31+
}
32+
33+
export function getStaticCredentialsDataErrors(
34+
data: StaticCredentialsProfileData
35+
): StaticCredentialsErrorResult | undefined {
36+
const accessKeyIdError = CredentialsKeyFormatValidators[SharedCredentialsKeys.AWS_ACCESS_KEY_ID](
37+
data[SharedCredentialsKeys.AWS_ACCESS_KEY_ID]
38+
)
39+
const secretAccessKeyError = CredentialsKeyFormatValidators[SharedCredentialsKeys.AWS_SECRET_ACCESS_KEY](
40+
data[SharedCredentialsKeys.AWS_SECRET_ACCESS_KEY]
41+
)
42+
43+
if (accessKeyIdError === undefined && secretAccessKeyError === undefined) {
44+
return undefined
45+
}
46+
47+
return {
48+
[SharedCredentialsKeys.AWS_ACCESS_KEY_ID]: accessKeyIdError,
49+
[SharedCredentialsKeys.AWS_SECRET_ACCESS_KEY]: secretAccessKeyError,
50+
}
51+
}
52+
53+
const accessKeyPattern = /[\w]{16,128}/
54+
55+
function getAccessKeyIdFormatError(awsAccessKeyId: string | undefined): string | undefined {
56+
if (awsAccessKeyId === undefined) {
57+
return undefined
58+
}
59+
60+
if (awsAccessKeyId === '') {
61+
return localize('AWS.credentials.error.emptyAccessKey', 'Access key must not be empty')
62+
}
63+
if (!accessKeyPattern.test(awsAccessKeyId)) {
64+
return localize(
65+
'AWS.credentials.error.emptyAccessKey',
66+
'Access key must be alphanumeric and between 16 and 128 characters'
67+
)
68+
}
69+
}
70+
71+
function getSecretAccessKeyFormatError(awsSecretAccessKey: string | undefined): string | undefined {
72+
if (awsSecretAccessKey === undefined) {
73+
return undefined
74+
}
75+
76+
if (awsSecretAccessKey === '') {
77+
return localize('AWS.credentials.error.emptySecretKey', 'Secret key must not be empty')
78+
}
79+
}
80+
81+
/** To be used as a sanity check to validate all core parts of a credentials profile */
82+
export async function validateCredentialsProfile(profileName: SectionName, profileData: StaticCredentialsProfileData) {
83+
if (await profileExists(profileName)) {
84+
throw new ToolkitError(`Credentials profile "${profileName}" already exists`)
85+
}
86+
87+
const credentialsDataErrors = getStaticCredentialsDataErrors(profileData)
88+
if (credentialsDataErrors !== undefined) {
89+
throw new ToolkitError(`Errors in credentials data: ${credentialsDataErrors}`, {
90+
code: 'InvalidCredentialsData',
91+
details: credentialsDataErrors,
92+
})
93+
}
94+
}

src/credentials/types.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,10 @@ export interface StaticCredentialsProfileData {
3333
[SharedCredentialsKeys.AWS_ACCESS_KEY_ID]: string
3434
[SharedCredentialsKeys.AWS_SECRET_ACCESS_KEY]: string
3535
}
36+
37+
/**
38+
* The name of a section in a credentials/config file
39+
*
40+
* The is the value of `{A}` in `[ {A} ]` or `[ {B} {A} ]`.
41+
*/
42+
export type SectionName = string

src/credentials/wizards/templates.ts

Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,12 @@ import { ProfileTemplateProvider } from './createProfile'
1212
import { createCommonButtons } from '../../shared/ui/buttons'
1313
import { credentialHelpUrl } from '../../shared/constants'
1414
import { SharedCredentialsKeys, StaticCredentialsProfileData } from '../types'
15+
import { CredentialsKeyFormatValidators } from '../sharedCredentialsValidation'
1516

1617
function getTitle(profileName: string): string {
1718
return localize('AWS.title.createCredentialProfile', 'Creating new profile "{0}"', profileName)
1819
}
1920

20-
const accessKeyPattern = /[\w]{16,128}/
21-
2221
export const staticCredentialsTemplate: ProfileTemplateProvider<StaticCredentialsProfileData> = {
2322
label: 'Static Credentials',
2423
description: 'Use this for credentials that never expire',
@@ -34,17 +33,7 @@ export const staticCredentialsTemplate: ProfileTemplateProvider<StaticCredential
3433
'Input the {0} Access Key',
3534
getIdeProperties().company
3635
),
37-
validateInput: accessKey => {
38-
if (accessKey === '') {
39-
return localize('AWS.credentials.error.emptyAccessKey', 'Access key must not be empty')
40-
}
41-
if (!accessKeyPattern.test(accessKey)) {
42-
return localize(
43-
'AWS.credentials.error.emptyAccessKey',
44-
'Access key must be alphanumeric and between 16 and 128 characters'
45-
)
46-
}
47-
},
36+
validateInput: CredentialsKeyFormatValidators.aws_access_key_id,
4837
}),
4938
[SharedCredentialsKeys.AWS_SECRET_ACCESS_KEY]: name =>
5039
createInputBox({
@@ -57,11 +46,7 @@ export const staticCredentialsTemplate: ProfileTemplateProvider<StaticCredential
5746
'Input the {0} Secret Key',
5847
getIdeProperties().company
5948
),
60-
validateInput: secretKey => {
61-
if (secretKey === '') {
62-
return localize('AWS.credentials.error.emptySecretKey', 'Secret key must not be empty')
63-
}
64-
},
49+
validateInput: CredentialsKeyFormatValidators.aws_secret_access_key,
6550
password: true,
6651
}),
6752
},

0 commit comments

Comments
 (0)