Skip to content

Commit 36c0b1f

Browse files
refactor: Credentials Validation (#3424)
This refactors the methods of credentials validation to instead have a single function that decides which validation method we want to use based off the key. Signed-off-by: Nikolas Komonen <[email protected]>
1 parent 2a3540a commit 36c0b1f

File tree

5 files changed

+109
-68
lines changed

5 files changed

+109
-68
lines changed

src/credentials/auth.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@ 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, StaticCredentialsProfileData } from './types'
50-
import { validateCredentialsProfile } from './sharedCredentialsValidation'
49+
import { SectionName, StaticCredentialsProfileKeys } from './types'
50+
import { throwOnInvalidCredentials } from './sharedCredentialsValidation'
5151

5252
export const ssoScope = 'sso:account:access'
5353
export const codecatalystScopes = ['codecatalyst:read_write']
@@ -1204,10 +1204,10 @@ const addConnection = Commands.register('aws.auth.addConnection', async () => {
12041204

12051205
export async function tryAddCredentials(
12061206
profileName: SectionName,
1207-
profileData: StaticCredentialsProfileData,
1207+
profileData: StaticCredentialsProfileKeys,
12081208
tryConnect = true
12091209
): Promise<boolean> {
1210-
await validateCredentialsProfile(profileName, profileData)
1210+
await throwOnInvalidCredentials(profileName, profileData)
12111211
await saveProfileToCredentials(profileName, profileData)
12121212
if (tryConnect) {
12131213
const auth = Auth.instance

src/credentials/sharedCredentials.ts

Lines changed: 2 additions & 2 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, StaticCredentialsProfileData } from './types'
11+
import { SectionName, StaticCredentialsProfileKeys } from './types'
1212
import { UserCredentialsUtils } from '../shared/credentials/userCredentialsUtils'
1313

1414
export async function updateAwsSdkLoadConfigEnvVar(): Promise<void> {
@@ -241,7 +241,7 @@ async function loadCredentialsFile(credentialsUri?: vscode.Uri): Promise<ReturnT
241241
*/
242242
export async function saveProfileToCredentials(
243243
profileName: SectionName,
244-
profileData: StaticCredentialsProfileData
244+
profileData: StaticCredentialsProfileKeys
245245
): Promise<void> {
246246
if (await profileExists(profileName)) {
247247
throw new ToolkitError(`Cannot save profile "${profileName}" because it already exists.`, {

src/credentials/sharedCredentialsValidation.ts

Lines changed: 91 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -6,89 +6,127 @@
66
*/
77

88
import { localize } from 'vscode-nls'
9-
import { SectionName, SharedCredentialsKeys, StaticCredentialsProfileData } from './types'
9+
import { CredentialsData, CredentialsKey, SectionName, SharedCredentialsKeys } from './types'
1010
import { ToolkitError } from '../shared/errors'
1111
import { profileExists } from './sharedCredentials'
12+
import { getLogger } from '../shared/logger'
13+
14+
/** credentials keys and their associated error message, if they exists */
15+
type CredentialsErrors = CredentialsData
1216

1317
/**
14-
* The format validators for shared credentials keys.
18+
* A function that validates a credential value
1519
*
16-
* A format validator validates the format of the data,
17-
* but not the validity of the content.
20+
* @returns An error message string if there is an error, otherwise undefined.
1821
*/
19-
export const CredentialsKeyFormatValidators = {
20-
[SharedCredentialsKeys.AWS_ACCESS_KEY_ID]: getAccessKeyIdFormatError,
21-
[SharedCredentialsKeys.AWS_SECRET_ACCESS_KEY]: getSecretAccessKeyFormatError,
22-
} as const
22+
type GetCredentialError = (key: CredentialsKey, value: string | undefined) => string | undefined
2323

2424
/**
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.
25+
* Validates all credentials values from the given input
26+
*
27+
* @returns Returns the same shape as the input, but the value of each
28+
* key is an error message if the original value is invalid,
29+
* otherwise undefined.
30+
*
31+
* If there are no errors at all, undefined is returned
2832
*/
29-
export type StaticCredentialsErrorResult = {
30-
[k in keyof StaticCredentialsProfileData]: string | undefined
33+
export function getCredentialsErrors(
34+
data: CredentialsData,
35+
validateFunc: GetCredentialError = getCredentialError
36+
): CredentialsErrors | undefined {
37+
const errors: CredentialsData = {}
38+
Object.entries(data).forEach(([key, value]) => {
39+
if (!isCredentialsKey(key)) {
40+
return
41+
}
42+
errors[key] = validateFunc(key, value)
43+
})
44+
if (Object.keys(errors).length === 0) {
45+
return
46+
}
47+
return errors
3148
}
3249

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
50+
export const getCredentialError: GetCredentialError = (key: CredentialsKey, value: string | undefined) => {
51+
const emptyError = getCredentialEmptyError(key, value)
52+
if (emptyError) {
53+
return emptyError
4554
}
4655

47-
return {
48-
[SharedCredentialsKeys.AWS_ACCESS_KEY_ID]: accessKeyIdError,
49-
[SharedCredentialsKeys.AWS_SECRET_ACCESS_KEY]: secretAccessKeyError,
56+
// If value is allowed to be empty, no need to validate anything further
57+
if (!value) {
58+
return
5059
}
51-
}
5260

53-
const accessKeyPattern = /[\w]{16,128}/
61+
return getCredentialFormatError(key, value)
62+
}
5463

55-
function getAccessKeyIdFormatError(awsAccessKeyId: string | undefined): string | undefined {
56-
if (awsAccessKeyId === undefined) {
57-
return undefined
64+
/**
65+
* Validates the format of a credential value.
66+
*
67+
* This function assumes there is a value to evaluate, if not
68+
* it returns no error.
69+
*/
70+
export const getCredentialFormatError: GetCredentialError = (key, value) => {
71+
if (!value) {
72+
/** Empty values should be validated in {@link getCredentialEmptyError} */
73+
getLogger().debug('getCredentialFormatError() called with empty value for key "%s"', key)
74+
return
5875
}
5976

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-
)
77+
switch (key) {
78+
case SharedCredentialsKeys.AWS_ACCESS_KEY_ID: {
79+
const accessKeyPattern = /[\w]{16,128}/
80+
if (!accessKeyPattern.test(value)) {
81+
return localize(
82+
'AWS.credentials.error.invalidAccessKeyFormat',
83+
'Access key must be alphanumeric and between 16 and 128 characters'
84+
)
85+
}
86+
return
87+
}
88+
case SharedCredentialsKeys.AWS_SECRET_ACCESS_KEY:
89+
return
90+
default:
91+
throw new ToolkitError(`Unsupported key in getCredentialFormatError(): "${key}"`)
6892
}
6993
}
7094

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')
95+
export const getCredentialEmptyError: GetCredentialError = (key: CredentialsKey, value: string | undefined) => {
96+
switch (key) {
97+
case SharedCredentialsKeys.AWS_ACCESS_KEY_ID:
98+
case SharedCredentialsKeys.AWS_SECRET_ACCESS_KEY:
99+
if (value) {
100+
return undefined
101+
}
102+
return 'Cannot be empty.'
103+
default:
104+
throw new ToolkitError(`Unsupported key in getCredentialEmptyError(): "${key}"`)
78105
}
79106
}
80107

81108
/** 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-
}
109+
export async function throwOnInvalidCredentials(profileName: SectionName, data: CredentialsData) {
110+
await validateProfileName(profileName)
86111

87-
const credentialsDataErrors = getStaticCredentialsDataErrors(profileData)
112+
const credentialsDataErrors = getCredentialsErrors(data)
88113
if (credentialsDataErrors !== undefined) {
89114
throw new ToolkitError(`Errors in credentials data: ${credentialsDataErrors}`, {
90115
code: 'InvalidCredentialsData',
91116
details: credentialsDataErrors,
92117
})
93118
}
94119
}
120+
121+
async function validateProfileName(profileName: SectionName) {
122+
if (await profileExists(profileName)) {
123+
throw new ToolkitError(`Credentials profile "${profileName}" already exists`)
124+
}
125+
}
126+
127+
// All shared credentials keys
128+
const sharedCredentialsKeysSet = new Set(Object.values(SharedCredentialsKeys))
129+
130+
export function isCredentialsKey(key: string): key is CredentialsKey {
131+
return sharedCredentialsKeysSet.has(key as CredentialsKey)
132+
}

src/credentials/types.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,18 @@ export const SharedCredentialsKeys = {
2424
SSO_REGISTRATION_SCOPES: 'sso_registration_scopes',
2525
} as const
2626

27+
/** An object that has only credentials data */
28+
export type CredentialsData = Partial<Record<CredentialsKey, string>>
29+
30+
export type CredentialsKey = (typeof SharedCredentialsKeys)[keyof typeof SharedCredentialsKeys]
31+
2732
/**
2833
* The required keys for a static credentials profile
2934
*
3035
* https://docs.aws.amazon.com/sdkref/latest/guide/feature-static-credentials.html
3136
*/
32-
export interface StaticCredentialsProfileData {
33-
[SharedCredentialsKeys.AWS_ACCESS_KEY_ID]: string
34-
[SharedCredentialsKeys.AWS_SECRET_ACCESS_KEY]: string
35-
}
37+
export type StaticCredentialsProfileKeysOptional = Pick<CredentialsData, 'aws_access_key_id' | 'aws_secret_access_key'>
38+
export type StaticCredentialsProfileKeys = Required<StaticCredentialsProfileKeysOptional>
3639

3740
/**
3841
* The name of a section in a credentials/config file

src/credentials/wizards/templates.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,14 @@ import { getIdeProperties } from '../../shared/extensionUtilities'
1111
import { ProfileTemplateProvider } from './createProfile'
1212
import { createCommonButtons } from '../../shared/ui/buttons'
1313
import { credentialHelpUrl } from '../../shared/constants'
14-
import { SharedCredentialsKeys, StaticCredentialsProfileData } from '../types'
15-
import { CredentialsKeyFormatValidators } from '../sharedCredentialsValidation'
14+
import { SharedCredentialsKeys, StaticCredentialsProfileKeys } from '../types'
15+
import { getCredentialError } from '../sharedCredentialsValidation'
1616

1717
function getTitle(profileName: string): string {
1818
return localize('AWS.title.createCredentialProfile', 'Creating new profile "{0}"', profileName)
1919
}
2020

21-
export const staticCredentialsTemplate: ProfileTemplateProvider<StaticCredentialsProfileData> = {
21+
export const staticCredentialsTemplate: ProfileTemplateProvider<StaticCredentialsProfileKeys> = {
2222
label: 'Static Credentials',
2323
description: 'Use this for credentials that never expire',
2424
prompts: {
@@ -33,7 +33,7 @@ export const staticCredentialsTemplate: ProfileTemplateProvider<StaticCredential
3333
'Input the {0} Access Key',
3434
getIdeProperties().company
3535
),
36-
validateInput: CredentialsKeyFormatValidators.aws_access_key_id,
36+
validateInput: value => getCredentialError(SharedCredentialsKeys.AWS_ACCESS_KEY_ID, value),
3737
}),
3838
[SharedCredentialsKeys.AWS_SECRET_ACCESS_KEY]: name =>
3939
createInputBox({
@@ -46,7 +46,7 @@ export const staticCredentialsTemplate: ProfileTemplateProvider<StaticCredential
4646
'Input the {0} Secret Key',
4747
getIdeProperties().company
4848
),
49-
validateInput: CredentialsKeyFormatValidators.aws_secret_access_key,
49+
validateInput: value => getCredentialError(SharedCredentialsKeys.AWS_SECRET_ACCESS_KEY, value),
5050
password: true,
5151
}),
5252
},

0 commit comments

Comments
 (0)