@@ -6,20 +6,18 @@ package software.aws.toolkits.jetbrains.core.credentials
6
6
import com.intellij.openapi.progress.ProcessCanceledException
7
7
import com.intellij.openapi.project.Project
8
8
import com.intellij.openapi.vfs.VirtualFileManager
9
- import org.slf4j.LoggerFactory
10
- import org.slf4j.event.Level
11
9
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials
12
10
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider
13
11
import software.amazon.awssdk.profiles.Profile
14
12
import software.amazon.awssdk.profiles.ProfileProperty
13
+ import software.amazon.awssdk.profiles.internal.ProfileFileReader
15
14
import software.amazon.awssdk.regions.Region
16
15
import software.amazon.awssdk.services.ssooidc.model.InvalidGrantException
17
16
import software.amazon.awssdk.services.ssooidc.model.InvalidRequestException
18
17
import software.amazon.awssdk.services.ssooidc.model.SsoOidcException
19
18
import software.amazon.awssdk.services.sts.StsClient
20
19
import software.aws.toolkits.core.credentials.validatedSsoIdentifierFromUrl
21
20
import software.aws.toolkits.core.region.AwsRegion
22
- import software.aws.toolkits.core.utils.tryOrNull
23
21
import software.aws.toolkits.jetbrains.core.AwsClientManager
24
22
import software.aws.toolkits.jetbrains.core.credentials.profiles.SsoSessionConstants
25
23
import software.aws.toolkits.jetbrains.core.credentials.sono.SONO_REGION
@@ -30,22 +28,30 @@ import software.aws.toolkits.resources.AwsCoreBundle
30
28
import software.aws.toolkits.telemetry.CredentialSourceId
31
29
import java.io.IOException
32
30
33
- private val LOG = LoggerFactory .getLogger(" LoginUtils" )
34
-
35
- sealed interface Login {
36
- val id: CredentialSourceId
31
+ sealed class Login <T > {
32
+ abstract val id: CredentialSourceId
33
+ abstract val onError: (Exception ) -> Unit
34
+ protected abstract fun doLogin (project : Project ): T
35
+
36
+ fun login (project : Project ): T {
37
+ try {
38
+ return doLogin(project)
39
+ } catch (e: Exception ) {
40
+ onError(e)
41
+ throw e
42
+ }
43
+ }
37
44
38
45
data class BuilderId (
39
46
val scopes : List <String >,
40
47
val onPendingToken : (InteractiveBearerTokenProvider ) -> Unit ,
41
- val onError : (Exception ) -> Unit ,
48
+ override val onError : (Exception ) -> Unit ,
42
49
val onSuccess : () -> Unit
43
- ) : Login {
50
+ ) : Login<Unit>() {
44
51
override val id: CredentialSourceId = CredentialSourceId .AwsId
45
52
46
- fun loginBuilderId (project : Project ): Boolean {
47
- loginSso(project, SONO_URL , SONO_REGION , scopes, onPendingToken, onError, onSuccess)
48
- return true
53
+ override fun doLogin (project : Project ) {
54
+ loginSso(project, SONO_URL , SONO_REGION , scopes, onPendingToken, onError, onSuccess) != null
49
55
}
50
56
}
51
57
@@ -55,16 +61,19 @@ sealed interface Login {
55
61
val scopes : List <String >,
56
62
val onPendingToken : (InteractiveBearerTokenProvider ) -> Unit ,
57
63
val onSuccess : () -> Unit ,
58
- val onError : (Exception , AuthProfile ) -> Unit
59
- ) : Login {
64
+ override val onError : (Exception ) -> Unit
65
+ ) : Login<AwsBearerTokenConnection?>() {
60
66
override val id: CredentialSourceId = CredentialSourceId .IamIdentityCenter
61
67
private val configFilesFacade = DefaultConfigFilesFacade ()
62
68
63
- fun loginIdc (project : Project ): AwsBearerTokenConnection ? {
69
+ override fun doLogin (project : Project ): AwsBearerTokenConnection ? {
64
70
// we have this check here so we blow up early if user has an invalid config file
65
- LOG .tryOrNull( " Failed to read sso sessions file " , level = Level . ERROR ) {
71
+ try {
66
72
configFilesFacade.readSsoSessions()
67
- } ? : return null
73
+ } catch (e: Exception ) {
74
+ onError(ConfigFacadeException (e))
75
+ return null
76
+ }
68
77
69
78
val profile = UserConfigSsoSessionProfile (
70
79
configSessionName = validatedSsoIdentifierFromUrl(startUrl),
@@ -73,6 +82,7 @@ sealed interface Login {
73
82
scopes = scopes
74
83
)
75
84
85
+ // expect 'authAndUpdateConfig' to call onError on failure
76
86
val conn = authAndUpdateConfig(project, profile, configFilesFacade, onPendingToken, onSuccess, onError) ? : return null
77
87
78
88
// TODO: delta, make sure we are good to switch immediately
@@ -90,21 +100,21 @@ sealed interface Login {
90
100
data class LongLivedIAM (
91
101
val profileName : String ,
92
102
val accessKey : String ,
93
- val secretKey : String
94
- ) : Login {
103
+ val secretKey : String ,
104
+ val onConfigFileFacadeError : (Exception ) -> Unit ,
105
+ val onProfileAlreadyExist : () -> Unit ,
106
+ val onConnectionValidationError : (Exception ) -> Unit
107
+ ) : Login<Boolean>() {
108
+ override val onError: (Exception ) -> Unit = {}
109
+
95
110
override val id: CredentialSourceId = CredentialSourceId .SharedCredentials
96
111
private val configFilesFacade = DefaultConfigFilesFacade ()
97
112
98
- fun loginIAM (
99
- project : Project ,
100
- onConfigFileFacadeError : (Exception ) -> Unit ,
101
- onProfileAlreadyExist : () -> Unit ,
102
- onConnectionValidationError : () -> Unit
103
- ): Boolean {
113
+ override fun doLogin (project : Project ): Boolean {
104
114
val existingProfiles = try {
105
115
configFilesFacade.readAllProfiles()
106
116
} catch (e: Exception ) {
107
- onConfigFileFacadeError(e )
117
+ onConfigFileFacadeError(ConfigFacadeException (e) )
108
118
return false
109
119
}
110
120
@@ -113,7 +123,7 @@ sealed interface Login {
113
123
return false
114
124
}
115
125
116
- val callerIdentity = tryOrNull {
126
+ try {
117
127
runUnderProgressIfNeeded(project, AwsCoreBundle .message(" settings.states.validating.short" ), cancelable = true ) {
118
128
AwsClientManager .getInstance().createUnmanagedClient<StsClient >(
119
129
StaticCredentialsProvider .create(AwsBasicCredentials .create(accessKey, secretKey)),
@@ -122,10 +132,8 @@ sealed interface Login {
122
132
client.getCallerIdentity()
123
133
}
124
134
}
125
- }
126
-
127
- if (callerIdentity == null ) {
128
- onConnectionValidationError()
135
+ } catch (e: Exception ) {
136
+ onConnectionValidationError(e)
129
137
return false
130
138
}
131
139
@@ -156,7 +164,7 @@ fun authAndUpdateConfig(
156
164
configFilesFacade : ConfigFilesFacade ,
157
165
onPendingToken : (InteractiveBearerTokenProvider ) -> Unit ,
158
166
onSuccess : () -> Unit ,
159
- onError : (Exception , AuthProfile ) -> Unit
167
+ onError : (Exception ) -> Unit
160
168
): AwsBearerTokenConnection ? {
161
169
val requestedScopes = profile.scopes
162
170
val allScopes = requestedScopes.toMutableSet()
@@ -181,7 +189,7 @@ fun authAndUpdateConfig(
181
189
reauthConnectionIfNeeded(project, connection, onPendingToken)
182
190
}
183
191
} catch (e: Exception ) {
184
- onError(e, profile )
192
+ onError(e)
185
193
return null
186
194
}
187
195
@@ -202,11 +210,12 @@ fun authAndUpdateConfig(
202
210
return connection
203
211
}
204
212
205
- internal fun ssoErrorMessageFromException (e : Exception ) = when (e) {
213
+ fun ssoErrorMessageFromException (e : Exception ) = when (e) {
206
214
is IllegalStateException -> e.message ? : AwsCoreBundle .message(" general.unknown_error" )
207
215
is ProcessCanceledException -> AwsCoreBundle .message(" codewhisperer.credential.login.dialog.exception.cancel_login" )
208
216
is InvalidRequestException -> AwsCoreBundle .message(" codewhisperer.credential.login.exception.invalid_input" )
209
217
is InvalidGrantException , is SsoOidcException -> e.message ? : AwsCoreBundle .message(" codewhisperer.credential.login.exception.invalid_grant" )
218
+ is ConfigFacadeException -> e.message
210
219
else -> {
211
220
val baseMessage = when (e) {
212
221
is IOException -> " codewhisperer.credential.login.exception.io"
@@ -216,3 +225,23 @@ internal fun ssoErrorMessageFromException(e: Exception) = when (e) {
216
225
AwsCoreBundle .message(baseMessage, " ${e.javaClass.name} : ${e.message} " )
217
226
}
218
227
}
228
+
229
+ class ConfigFacadeException (override val cause : Exception ) : Exception() {
230
+ override val message: String
231
+ get() = messageFromConfigFacadeError(cause).first
232
+
233
+ override fun getStackTrace () = cause.stackTrace
234
+ }
235
+
236
+ fun messageFromConfigFacadeError (e : Exception ): Pair <String , String > {
237
+ // we'll consider nested exceptions and exception loops to be out of scope
238
+ val (errorTemplate, errorType) = if (e.stackTrace.any { it.className == ProfileFileReader ::class .java.canonicalName }) {
239
+ " gettingstarted.auth.config.issue" to " ConfigParseError"
240
+ } else {
241
+ " codewhisperer.credential.login.exception.general" to e::class .java.name
242
+ }
243
+
244
+ val errorMessage = AwsCoreBundle .message(errorTemplate, e.localizedMessage ? : e::class .java.name)
245
+
246
+ return errorMessage to errorType
247
+ }
0 commit comments