@@ -14,7 +14,7 @@ import { Credentials } from '@aws-sdk/types'
14
14
import { SsoAccessTokenProvider } from './sso/ssoAccessTokenProvider'
15
15
import { codicon , getIcon } from '../shared/icons'
16
16
import { Commands } from '../shared/vscode/commands2'
17
- import { DataQuickPickItem , showQuickPick } from '../shared/ui/pickerPrompter'
17
+ import { createQuickPick , DataQuickPickItem , showQuickPick } from '../shared/ui/pickerPrompter'
18
18
import { isValidResponse } from '../shared/wizards/wizard'
19
19
import { CancellationError } from '../shared/utilities/timeoutUtils'
20
20
import { ToolkitError , UnknownError } from '../shared/errors'
@@ -32,11 +32,12 @@ import { TreeNode } from '../shared/treeview/resourceTreeDataProvider'
32
32
import { createInputBox } from '../shared/ui/inputPrompter'
33
33
import { CredentialsSettings } from './credentialsUtilities'
34
34
import { telemetry } from '../shared/telemetry/telemetry'
35
- import { createCommonButtons , createExitButton , createHelpButton } from '../shared/ui/buttons'
35
+ import { createCommonButtons , createExitButton , createHelpButton , createRefreshButton } from '../shared/ui/buttons'
36
36
import { getIdeProperties , isCloud9 } from '../shared/extensionUtilities'
37
37
import { getCodeCatalystDevEnvId } from '../shared/vscode/env'
38
38
import { getConfigFilename } from './sharedCredentials'
39
39
import { authHelpUrl } from '../shared/constants'
40
+ import { getDependentAuths } from './secondaryAuth'
40
41
41
42
export const ssoScope = 'sso:account:access'
42
43
export const codecatalystScopes = [ 'codecatalyst:read_write' ]
@@ -296,12 +297,19 @@ function sortProfilesByScope(profiles: StoredProfile<Profile>[]): StoredProfile<
296
297
297
298
// The true connection state can only be known after trying to use the connection
298
299
// So it is not exposed on the `Connection` interface
299
- type StatefulConnection = Connection & { readonly state : ProfileMetadata [ 'connectionState' ] }
300
+ export type StatefulConnection = Connection & { readonly state : ProfileMetadata [ 'connectionState' ] }
301
+
302
+ interface ConnectionStateChangeEvent {
303
+ readonly id : Connection [ 'id' ]
304
+ readonly state : ProfileMetadata [ 'connectionState' ]
305
+ }
300
306
301
307
export class Auth implements AuthService , ConnectionManager {
302
308
private readonly ssoCache = getCache ( )
303
- private readonly onDidChangeActiveConnectionEmitter = new vscode . EventEmitter < StatefulConnection | undefined > ( )
304
- public readonly onDidChangeActiveConnection = this . onDidChangeActiveConnectionEmitter . event
309
+ readonly #onDidChangeActiveConnection = new vscode . EventEmitter < StatefulConnection | undefined > ( )
310
+ readonly #onDidChangeConnectionState = new vscode . EventEmitter < ConnectionStateChangeEvent > ( )
311
+ public readonly onDidChangeActiveConnection = this . #onDidChangeActiveConnection. event
312
+ public readonly onDidChangeConnectionState = this . #onDidChangeConnectionState. event
305
313
306
314
public constructor (
307
315
private readonly store : ProfileStore ,
@@ -363,7 +371,7 @@ export class Auth implements AuthService, ConnectionManager {
363
371
: this . getIamConnection ( id , await this . getCredentialsProvider ( id ) )
364
372
365
373
this . #activeConnection = conn
366
- this . onDidChangeActiveConnectionEmitter . fire ( conn )
374
+ this . #onDidChangeActiveConnection . fire ( conn )
367
375
await this . store . setCurrentProfileId ( id )
368
376
369
377
return conn
@@ -377,7 +385,7 @@ export class Auth implements AuthService, ConnectionManager {
377
385
await this . store . setCurrentProfileId ( undefined )
378
386
await this . invalidateConnection ( this . activeConnection . id )
379
387
this . #activeConnection = undefined
380
- this . onDidChangeActiveConnectionEmitter . fire ( undefined )
388
+ this . #onDidChangeActiveConnection . fire ( undefined )
381
389
}
382
390
383
391
public async listConnections ( ) : Promise < Connection [ ] > {
@@ -485,8 +493,9 @@ export class Auth implements AuthService, ConnectionManager {
485
493
const profile = await this . store . updateProfile ( id , { connectionState } )
486
494
if ( this . #activeConnection?. id === id ) {
487
495
this . #activeConnection. state = connectionState
488
- this . onDidChangeActiveConnectionEmitter . fire ( this . #activeConnection)
496
+ this . #onDidChangeActiveConnection . fire ( this . #activeConnection)
489
497
}
498
+ this . #onDidChangeConnectionState. fire ( { id, state : connectionState } )
490
499
491
500
return profile
492
501
}
@@ -656,7 +665,7 @@ export class Auth implements AuthService, ConnectionManager {
656
665
code : 'InvalidConnection' ,
657
666
} )
658
667
}
659
-
668
+ // TODO: cancellable notification?
660
669
if ( previousState === 'valid' ) {
661
670
const message = localize ( 'aws.auth.invalidConnection' , 'Connection is invalid or expired, login again?' )
662
671
const resp = await vscode . window . showInformationMessage ( message , localizedText . yes , localizedText . no )
@@ -723,56 +732,8 @@ export class Auth implements AuthService, ConnectionManager {
723
732
}
724
733
}
725
734
726
- const getConnectionIcon = ( conn : Connection ) =>
727
- conn . type === 'sso' ? getIcon ( 'vscode-account' ) : getIcon ( 'vscode-key' )
728
-
729
- function toPickerItem ( conn : Connection ) {
730
- const label = codicon `${ getConnectionIcon ( conn ) } ${ conn . label } `
731
- const descPrefix = conn . type === 'iam' ? 'IAM Credential' : undefined
732
- const descSuffix = conn . id . startsWith ( 'profile:' )
733
- ? 'configured locally (~/.aws/config)'
734
- : 'sourced from the environment'
735
-
736
- return {
737
- label,
738
- data : conn ,
739
- description : descPrefix !== undefined ? `${ descPrefix } , ${ descSuffix } ` : undefined ,
740
- }
741
- }
742
-
743
735
export async function promptForConnection ( auth : Auth , type ?: 'iam' | 'sso' ) {
744
- const addNewConnection = {
745
- label : codicon `${ getIcon ( 'vscode-plus' ) } Add New Connection` ,
746
- data : 'addNewConnection' as const ,
747
- }
748
-
749
- const editCredentials = {
750
- label : codicon `${ getIcon ( 'vscode-pencil' ) } Edit Credentials` ,
751
- data : 'editCredentials' as const ,
752
- }
753
-
754
- const items = ( async function ( ) {
755
- // TODO: list linked connections
756
- const connections = await auth . listConnections ( )
757
- connections . sort ( ( a , b ) => ( a . type === 'sso' ? - 1 : b . type === 'sso' ? 1 : a . label . localeCompare ( b . label ) ) )
758
- const filtered = type !== undefined ? connections . filter ( c => c . type === type ) : connections
759
- const items = [ ...filtered . map ( toPickerItem ) , addNewConnection ]
760
- const canShowEdit = connections . filter ( isIamConnection ) . filter ( c => c . label . startsWith ( 'profile' ) ) . length > 0
761
-
762
- return canShowEdit ? [ ...items , editCredentials ] : items
763
- } ) ( )
764
-
765
- const placeholder =
766
- type === 'iam'
767
- ? localize ( 'aws.auth.promptConnection.iam.placeholder' , 'Select an IAM credential' )
768
- : localize ( 'aws.auth.promptConnection.all.placeholder' , 'Select a connection' )
769
-
770
- const resp = await showQuickPick < Connection | 'addNewConnection' | 'editCredentials' > ( items , {
771
- placeholder,
772
- title : localize ( 'aws.auth.promptConnection.title' , 'Switch Connection' ) ,
773
- buttons : createCommonButtons ( ) ,
774
- } )
775
-
736
+ const resp = await createConnectionPrompter ( auth , type ) . prompt ( )
776
737
if ( ! isValidResponse ( resp ) ) {
777
738
throw new CancellationError ( 'user' )
778
739
}
@@ -974,9 +935,132 @@ const addConnection = Commands.register('aws.auth.addConnection', async () => {
974
935
}
975
936
} )
976
937
977
- const reauth = Commands . register ( '_aws.auth.reauthenticate' , async ( auth : Auth , conn : Connection ) => {
938
+ const getConnectionIcon = ( conn : Connection ) =>
939
+ conn . type === 'sso' ? getIcon ( 'vscode-account' ) : getIcon ( 'vscode-key' )
940
+
941
+ export function createConnectionPrompter ( auth : Auth , type ?: 'iam' | 'sso' ) {
942
+ const placeholder =
943
+ type === 'iam'
944
+ ? localize ( 'aws.auth.promptConnection.iam.placeholder' , 'Select an IAM credential' )
945
+ : localize ( 'aws.auth.promptConnection.all.placeholder' , 'Select a connection' )
946
+
947
+ const refreshButton = createRefreshButton ( )
948
+ refreshButton . onClick = ( ) => void prompter . clearAndLoadItems ( loadItems ( ) )
949
+
950
+ const prompter = createQuickPick ( loadItems ( ) , {
951
+ placeholder,
952
+ title : localize ( 'aws.auth.promptConnection.title' , 'Switch Connection' ) ,
953
+ buttons : [ refreshButton , createExitButton ( ) ] ,
954
+ } )
955
+
956
+ return prompter
957
+
958
+ async function loadItems ( ) : Promise < DataQuickPickItem < Connection | 'addNewConnection' | 'editCredentials' > [ ] > {
959
+ const addNewConnection = {
960
+ label : codicon `${ getIcon ( 'vscode-plus' ) } Add New Connection` ,
961
+ data : 'addNewConnection' as const ,
962
+ }
963
+ const editCredentials = {
964
+ label : codicon `${ getIcon ( 'vscode-pencil' ) } Edit Credentials` ,
965
+ data : 'editCredentials' as const ,
966
+ }
967
+
968
+ // TODO: list linked connections
969
+ const connections = await auth . listConnections ( )
970
+
971
+ // Sort 'sso' connections first, then valid connections, then by label
972
+ const sortByState = ( a : Connection , b : Connection ) => {
973
+ const stateA = auth . getConnectionState ( a )
974
+ const stateB = auth . getConnectionState ( b )
975
+
976
+ return stateA === stateB
977
+ ? a . label . localeCompare ( b . label )
978
+ : stateA === 'valid'
979
+ ? - 1
980
+ : stateB === 'valid'
981
+ ? 1
982
+ : 0
983
+ }
984
+ connections . sort ( ( a , b ) =>
985
+ a . type === b . type ? sortByState ( a , b ) : a . type === 'sso' ? - 1 : b . type === 'sso' ? 1 : 0
986
+ )
987
+
988
+ const filtered = type !== undefined ? connections . filter ( c => c . type === type ) : connections
989
+ const items = [ ...filtered . map ( toPickerItem ) , addNewConnection ]
990
+ const canShowEdit = connections . filter ( isIamConnection ) . filter ( c => c . label . startsWith ( 'profile' ) ) . length > 0
991
+
992
+ return canShowEdit ? [ ...items , editCredentials ] : items
993
+ }
994
+
995
+ function toPickerItem ( conn : Connection ) : DataQuickPickItem < Connection > {
996
+ const state = auth . getConnectionState ( conn )
997
+ if ( state !== 'valid' ) {
998
+ return {
999
+ data : conn ,
1000
+ invalidSelection : true ,
1001
+ label : codicon `${ getIcon ( 'vscode-error' ) } ${ conn . label } ` ,
1002
+ description :
1003
+ state === 'authenticating'
1004
+ ? 'authenticating...'
1005
+ : localize (
1006
+ 'aws.auth.promptConnection.expired.description' ,
1007
+ 'Expired or Invalid, select to authenticate'
1008
+ ) ,
1009
+ onClick :
1010
+ state !== 'authenticating'
1011
+ ? async ( ) => {
1012
+ // XXX: this is hack because only 1 picker can be shown at a time
1013
+ // Some legacy auth providers will show a picker, hiding this one
1014
+ // If we detect this then we'll jump straight into using the connection
1015
+ let hidden = false
1016
+ const sub = prompter . quickPick . onDidHide ( ( ) => {
1017
+ hidden = true
1018
+ sub . dispose ( )
1019
+ } )
1020
+ const newConn = await reauthCommand . execute ( auth , conn )
1021
+ if ( hidden && newConn && auth . getConnectionState ( newConn ) === 'valid' ) {
1022
+ await auth . useConnection ( newConn )
1023
+ } else {
1024
+ await prompter . clearAndLoadItems ( loadItems ( ) )
1025
+ prompter . selectItems (
1026
+ ...prompter . quickPick . items . filter ( i => i . label . includes ( conn . label ) )
1027
+ )
1028
+ }
1029
+ }
1030
+ : undefined ,
1031
+ }
1032
+ }
1033
+
1034
+ return {
1035
+ data : conn ,
1036
+ label : codicon `${ getConnectionIcon ( conn ) } ${ conn . label } ` ,
1037
+ description : getConnectionDescription ( conn ) ,
1038
+ }
1039
+ }
1040
+
1041
+ function getConnectionDescription ( conn : Connection ) {
1042
+ if ( conn . type === 'iam' ) {
1043
+ const descSuffix = conn . id . startsWith ( 'profile:' )
1044
+ ? 'configured locally (~/.aws/config)'
1045
+ : 'sourced from the environment'
1046
+
1047
+ return `IAM Credential, ${ descSuffix } `
1048
+ }
1049
+
1050
+ const toolAuths = getDependentAuths ( conn )
1051
+ if ( toolAuths . length === 0 ) {
1052
+ return undefined
1053
+ } else if ( toolAuths . length === 1 ) {
1054
+ return `Connected to ${ toolAuths [ 0 ] . toolLabel } `
1055
+ } else {
1056
+ return `Connected to Dev Tools`
1057
+ }
1058
+ }
1059
+ }
1060
+
1061
+ export const reauthCommand = Commands . register ( '_aws.auth.reauthenticate' , async ( auth : Auth , conn : Connection ) => {
978
1062
try {
979
- await auth . reauthenticate ( conn )
1063
+ return await auth . reauthenticate ( conn )
980
1064
} catch ( err ) {
981
1065
throw ToolkitError . chain ( err , 'Unable to authenticate connection' )
982
1066
}
@@ -1049,7 +1133,7 @@ export class AuthNode implements TreeNode<Auth> {
1049
1133
this . setDescription ( item , 'authenticating...' )
1050
1134
} else {
1051
1135
this . setDescription ( item , 'expired or invalid, click to authenticate' )
1052
- item . command = reauth . build ( this . resource , conn ) . asCommand ( { title : 'Reauthenticate' } )
1136
+ item . command = reauthCommand . build ( this . resource , conn ) . asCommand ( { title : 'Reauthenticate' } )
1053
1137
}
1054
1138
} else {
1055
1139
item . command = switchConnections . build ( this . resource ) . asCommand ( { title : 'Login' } )
0 commit comments