Skip to content

Commit 4886406

Browse files
authored
Surface error popup dialog on login failure (#4780)
1 parent 182dc6a commit 4886406

File tree

7 files changed

+121
-76
lines changed

7 files changed

+121
-76
lines changed

plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/credentials/LoginUtils.kt

Lines changed: 63 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,18 @@ package software.aws.toolkits.jetbrains.core.credentials
66
import com.intellij.openapi.progress.ProcessCanceledException
77
import com.intellij.openapi.project.Project
88
import com.intellij.openapi.vfs.VirtualFileManager
9-
import org.slf4j.LoggerFactory
10-
import org.slf4j.event.Level
119
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials
1210
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider
1311
import software.amazon.awssdk.profiles.Profile
1412
import software.amazon.awssdk.profiles.ProfileProperty
13+
import software.amazon.awssdk.profiles.internal.ProfileFileReader
1514
import software.amazon.awssdk.regions.Region
1615
import software.amazon.awssdk.services.ssooidc.model.InvalidGrantException
1716
import software.amazon.awssdk.services.ssooidc.model.InvalidRequestException
1817
import software.amazon.awssdk.services.ssooidc.model.SsoOidcException
1918
import software.amazon.awssdk.services.sts.StsClient
2019
import software.aws.toolkits.core.credentials.validatedSsoIdentifierFromUrl
2120
import software.aws.toolkits.core.region.AwsRegion
22-
import software.aws.toolkits.core.utils.tryOrNull
2321
import software.aws.toolkits.jetbrains.core.AwsClientManager
2422
import software.aws.toolkits.jetbrains.core.credentials.profiles.SsoSessionConstants
2523
import software.aws.toolkits.jetbrains.core.credentials.sono.SONO_REGION
@@ -30,22 +28,30 @@ import software.aws.toolkits.resources.AwsCoreBundle
3028
import software.aws.toolkits.telemetry.CredentialSourceId
3129
import java.io.IOException
3230

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+
}
3744

3845
data class BuilderId(
3946
val scopes: List<String>,
4047
val onPendingToken: (InteractiveBearerTokenProvider) -> Unit,
41-
val onError: (Exception) -> Unit,
48+
override val onError: (Exception) -> Unit,
4249
val onSuccess: () -> Unit
43-
) : Login {
50+
) : Login<Unit>() {
4451
override val id: CredentialSourceId = CredentialSourceId.AwsId
4552

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
4955
}
5056
}
5157

@@ -55,16 +61,19 @@ sealed interface Login {
5561
val scopes: List<String>,
5662
val onPendingToken: (InteractiveBearerTokenProvider) -> Unit,
5763
val onSuccess: () -> Unit,
58-
val onError: (Exception, AuthProfile) -> Unit
59-
) : Login {
64+
override val onError: (Exception) -> Unit
65+
) : Login<AwsBearerTokenConnection?>() {
6066
override val id: CredentialSourceId = CredentialSourceId.IamIdentityCenter
6167
private val configFilesFacade = DefaultConfigFilesFacade()
6268

63-
fun loginIdc(project: Project): AwsBearerTokenConnection? {
69+
override fun doLogin(project: Project): AwsBearerTokenConnection? {
6470
// 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 {
6672
configFilesFacade.readSsoSessions()
67-
} ?: return null
73+
} catch (e: Exception) {
74+
onError(ConfigFacadeException(e))
75+
return null
76+
}
6877

6978
val profile = UserConfigSsoSessionProfile(
7079
configSessionName = validatedSsoIdentifierFromUrl(startUrl),
@@ -73,6 +82,7 @@ sealed interface Login {
7382
scopes = scopes
7483
)
7584

85+
// expect 'authAndUpdateConfig' to call onError on failure
7686
val conn = authAndUpdateConfig(project, profile, configFilesFacade, onPendingToken, onSuccess, onError) ?: return null
7787

7888
// TODO: delta, make sure we are good to switch immediately
@@ -90,21 +100,21 @@ sealed interface Login {
90100
data class LongLivedIAM(
91101
val profileName: String,
92102
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+
95110
override val id: CredentialSourceId = CredentialSourceId.SharedCredentials
96111
private val configFilesFacade = DefaultConfigFilesFacade()
97112

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 {
104114
val existingProfiles = try {
105115
configFilesFacade.readAllProfiles()
106116
} catch (e: Exception) {
107-
onConfigFileFacadeError(e)
117+
onConfigFileFacadeError(ConfigFacadeException(e))
108118
return false
109119
}
110120

@@ -113,7 +123,7 @@ sealed interface Login {
113123
return false
114124
}
115125

116-
val callerIdentity = tryOrNull {
126+
try {
117127
runUnderProgressIfNeeded(project, AwsCoreBundle.message("settings.states.validating.short"), cancelable = true) {
118128
AwsClientManager.getInstance().createUnmanagedClient<StsClient>(
119129
StaticCredentialsProvider.create(AwsBasicCredentials.create(accessKey, secretKey)),
@@ -122,10 +132,8 @@ sealed interface Login {
122132
client.getCallerIdentity()
123133
}
124134
}
125-
}
126-
127-
if (callerIdentity == null) {
128-
onConnectionValidationError()
135+
} catch (e: Exception) {
136+
onConnectionValidationError(e)
129137
return false
130138
}
131139

@@ -156,7 +164,7 @@ fun authAndUpdateConfig(
156164
configFilesFacade: ConfigFilesFacade,
157165
onPendingToken: (InteractiveBearerTokenProvider) -> Unit,
158166
onSuccess: () -> Unit,
159-
onError: (Exception, AuthProfile) -> Unit
167+
onError: (Exception) -> Unit
160168
): AwsBearerTokenConnection? {
161169
val requestedScopes = profile.scopes
162170
val allScopes = requestedScopes.toMutableSet()
@@ -181,7 +189,7 @@ fun authAndUpdateConfig(
181189
reauthConnectionIfNeeded(project, connection, onPendingToken)
182190
}
183191
} catch (e: Exception) {
184-
onError(e, profile)
192+
onError(e)
185193
return null
186194
}
187195

@@ -202,11 +210,12 @@ fun authAndUpdateConfig(
202210
return connection
203211
}
204212

205-
internal fun ssoErrorMessageFromException(e: Exception) = when (e) {
213+
fun ssoErrorMessageFromException(e: Exception) = when (e) {
206214
is IllegalStateException -> e.message ?: AwsCoreBundle.message("general.unknown_error")
207215
is ProcessCanceledException -> AwsCoreBundle.message("codewhisperer.credential.login.dialog.exception.cancel_login")
208216
is InvalidRequestException -> AwsCoreBundle.message("codewhisperer.credential.login.exception.invalid_input")
209217
is InvalidGrantException, is SsoOidcException -> e.message ?: AwsCoreBundle.message("codewhisperer.credential.login.exception.invalid_grant")
218+
is ConfigFacadeException -> e.message
210219
else -> {
211220
val baseMessage = when (e) {
212221
is IOException -> "codewhisperer.credential.login.exception.io"
@@ -216,3 +225,23 @@ internal fun ssoErrorMessageFromException(e: Exception) = when (e) {
216225
AwsCoreBundle.message(baseMessage, "${e.javaClass.name}: ${e.message}")
217226
}
218227
}
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+
}

plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/gettingstarted/SetupAuthenticationDialog.kt

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ import org.jetbrains.annotations.VisibleForTesting
2727
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials
2828
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider
2929
import software.amazon.awssdk.profiles.Profile
30-
import software.amazon.awssdk.profiles.internal.ProfileFileReader
3130
import software.amazon.awssdk.regions.Region
3231
import software.amazon.awssdk.services.sts.StsClient
3332
import software.aws.toolkits.core.region.AwsRegion
@@ -42,6 +41,7 @@ import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnectionManager
4241
import software.aws.toolkits.jetbrains.core.credentials.UserConfigSsoSessionProfile
4342
import software.aws.toolkits.jetbrains.core.credentials.authAndUpdateConfig
4443
import software.aws.toolkits.jetbrains.core.credentials.loginSso
44+
import software.aws.toolkits.jetbrains.core.credentials.messageFromConfigFacadeError
4545
import software.aws.toolkits.jetbrains.core.credentials.sono.IDENTITY_CENTER_ROLE_ACCESS_SCOPE
4646
import software.aws.toolkits.jetbrains.core.credentials.sono.SONO_REGION
4747
import software.aws.toolkits.jetbrains.core.credentials.sono.SONO_URL
@@ -277,7 +277,7 @@ class SetupAuthenticationDialog(
277277
scopes = scopes
278278
)
279279

280-
val connection = authAndUpdateConfig(project, profile, configFilesFacade, {}, {}) { e, _ ->
280+
val connection = authAndUpdateConfig(project, profile, configFilesFacade, {}, {}) { e ->
281281
Messages.showErrorDialog(project, e.message, title)
282282
AuthTelemetry.addConnection(
283283
project,
@@ -430,12 +430,7 @@ class SetupAuthenticationDialog(
430430
}
431431

432432
private fun handleConfigFacadeError(e: Exception) {
433-
// we'll consider nested exceptions and exception loops to be out of scope
434-
val (errorTemplate, errorType) = if (e.stackTrace.any { it.className == ProfileFileReader::class.java.canonicalName }) {
435-
"gettingstarted.auth.config.issue" to "ConfigParseError"
436-
} else {
437-
"codewhisperer.credential.login.exception.general" to e::class.java.name
438-
}
433+
val (errorMessage, errorType) = messageFromConfigFacadeError(e)
439434

440435
AuthTelemetry.addConnection(
441436
project,
@@ -448,9 +443,8 @@ class SetupAuthenticationDialog(
448443
reason = errorType
449444
)
450445

451-
val error = AwsCoreBundle.message(errorTemplate, e.localizedMessage ?: e::class.java.name)
452-
LOG.error(e) { error }
453-
Messages.showErrorDialog(project, error, title)
446+
LOG.error(e) { errorMessage }
447+
Messages.showErrorDialog(project, errorMessage, title)
454448
}
455449

456450
companion object {

0 commit comments

Comments
 (0)