Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"type" : "bugfix",
"description" : "Fix issue where a user may get stuck while attempting to login to Builder ID"
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,22 @@
// TODO: unify with AwsConnectionManager
@State(name = "connectionManager", storages = [Storage("aws.xml")])
class DefaultToolkitConnectionManager : ToolkitConnectionManager, PersistentStateComponent<ToolkitConnectionManagerState> {
private val project: Project?

constructor(project: Project) {
this.project = project
}

constructor() {

Check warning on line 28 in plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/credentials/DefaultToolkitConnectionManager.kt

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Unused symbol

Constructor is never used
this.project = null
}

Check warning on line 30 in plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/credentials/DefaultToolkitConnectionManager.kt

View check run for this annotation

Codecov / codecov/patch

plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/credentials/DefaultToolkitConnectionManager.kt#L28-L30

Added lines #L28 - L30 were not covered by tests

init {
ApplicationManager.getApplication().messageBus.connect(this).subscribe(
BearerTokenProviderListener.TOPIC,
object : BearerTokenProviderListener {
override fun invalidate(providerId: String) {
if (activeConnection()?.id == providerId) {

Check warning on line 37 in plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/credentials/DefaultToolkitConnectionManager.kt

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Usage of redundant or deprecated syntax or deprecated symbols

'activeConnection(): ToolkitConnection?' is deprecated. Fragile API. Probably leads to unexpected behavior. Use only for toolkit explorer dropdown state.
switchConnection(null)
ActivityTracker.getInstance().inc()
}
Expand All @@ -33,19 +43,10 @@
)
}

private val project: Project?

constructor(project: Project) {
this.project = project
}

constructor() {
this.project = null
}

private var connection: ToolkitConnection? = null

private val pinningManager: ConnectionPinningManager = ConnectionPinningManager.getInstance()
private val pinningManager
get() = ConnectionPinningManager.getInstance()

private val defaultConnection: ToolkitConnection?
get() {
Expand All @@ -60,6 +61,10 @@
return null
}

@Deprecated(
"Fragile API. Probably leads to unexpected behavior. Use only for toolkit explorer dropdown state.",
replaceWith = ReplaceWith("activeConnectionForFeature(feature)")
)
@Synchronized
override fun activeConnection() = connection ?: defaultConnection

Expand All @@ -71,11 +76,11 @@
}

return connection?.let {
if (feature.supportsConnectionType(it)) {
return it
return@let if (feature.supportsConnectionType(it)) {
it

Check warning on line 80 in plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/credentials/DefaultToolkitConnectionManager.kt

View check run for this annotation

Codecov / codecov/patch

plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/credentials/DefaultToolkitConnectionManager.kt#L80

Added line #L80 was not covered by tests
} else {
null

Check warning on line 82 in plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/credentials/DefaultToolkitConnectionManager.kt

View check run for this annotation

Codecov / codecov/patch

plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/credentials/DefaultToolkitConnectionManager.kt#L82

Added line #L82 was not covered by tests
}

null
} ?: defaultConnection?.let {
if (ApplicationInfo.getInstance().build.productCode == "GW") return null
if (feature.supportsConnectionType(it)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,10 @@ interface ToolkitStartupAuthFactory {
}

interface ToolkitConnectionManager : Disposable {
@Deprecated(
"Fragile API. Probably leads to unexpected behavior. Use only for toolkit explorer dropdown state.",
ReplaceWith("activeConnectionForFeature(feature)")
)
fun activeConnection(): ToolkitConnection?

fun activeConnectionForFeature(feature: FeatureWithPinnedConnection): ToolkitConnection?
Expand Down Expand Up @@ -129,6 +133,7 @@ fun loginSso(
project = project,
connection = transientConnection,
onPendingToken = onPendingToken,
isReAuth = false,
source = metadata?.sourceId,
)
}
Expand All @@ -149,7 +154,7 @@ fun loginSso(

val manager = ToolkitAuthManager.getInstance()
val allScopes = requestedScopes.toMutableSet()
return manager.getConnection(connectionId)?.let { connection ->
val connection = manager.getConnection(connectionId)?.let { connection ->
val logger = getLogger<ToolkitAuthManager>()

if (connection !is AwsBearerTokenConnection) {
Expand All @@ -167,32 +172,32 @@ fun loginSso(
""".trimIndent()
}
// can't reuse since requested scopes are not in current connection. forcing reauth
return createAndAuthNewConnection(
ManagedSsoProfile(
region,
startUrl,
allScopes.toList()
)
)
return@let null
}

// For the case when the existing connection is in invalid state, we need to re-auth
reauthConnectionIfNeeded(
project = project,
connection = connection,
isReAuth = true
onPendingToken = onPendingToken,
isReAuth = true,
source = metadata?.sourceId,
)
return@let connection
}

if (connection != null) {
return connection
} ?: run {
// No existing connection, start from scratch
createAndAuthNewConnection(
ManagedSsoProfile(
region,
startUrl,
allScopes.toList()
)
)
}

// No existing connection, start from scratch
return createAndAuthNewConnection(
ManagedSsoProfile(
region,
startUrl,
allScopes.toList()
)
)
}

@Suppress("UnusedParameter")
Expand Down Expand Up @@ -242,7 +247,9 @@ fun reauthConnectionIfNeeded(
}

val startUrl = (connection as AwsBearerTokenConnection).startUrl
var didReauth = false
maybeReauthProviderIfNeeded(project, tokenProvider) {
didReauth = true
runUnderProgressIfNeeded(project, AwsCoreBundle.message("credentials.pending.title"), true) {
try {
tokenProvider.reauthenticate()
Expand Down Expand Up @@ -283,6 +290,11 @@ fun reauthConnectionIfNeeded(
}
}
}

if (!didReauth) {
// webview is stuck if reauth was not needed (i.e. token on disk is valid)
project?.let { ToolkitConnectionManager.getInstance(it).switchConnection(connection) }
}
return tokenProvider
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,14 @@ import org.junit.jupiter.api.extension.RegisterExtension
import org.mockito.Mockito.mockConstruction
import org.mockito.kotlin.any
import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.atLeastOnce
import org.mockito.kotlin.doNothing
import org.mockito.kotlin.mock
import org.mockito.kotlin.spy
import org.mockito.kotlin.timeout
import org.mockito.kotlin.verify
import org.mockito.kotlin.verifyNoMoreInteractions
import org.mockito.kotlin.whenever
import software.amazon.awssdk.regions.Region
import software.amazon.awssdk.services.ssooidc.SsoOidcClient
import software.aws.toolkits.core.telemetry.MetricEvent
import software.aws.toolkits.core.telemetry.TelemetryBatcher
Expand All @@ -36,8 +36,6 @@ import software.aws.toolkits.jetbrains.core.credentials.profiles.ProfileSsoSessi
import software.aws.toolkits.jetbrains.core.credentials.sso.bearer.BearerTokenAuthState
import software.aws.toolkits.jetbrains.core.credentials.sso.bearer.BearerTokenProviderListener
import software.aws.toolkits.jetbrains.core.credentials.sso.bearer.InteractiveBearerTokenProvider
import software.aws.toolkits.jetbrains.core.region.MockRegionProviderExtension
import software.aws.toolkits.jetbrains.core.region.MockRegionProviderRule
import software.aws.toolkits.jetbrains.services.telemetry.NoOpPublisher
import software.aws.toolkits.jetbrains.services.telemetry.TelemetryService
import software.aws.toolkits.jetbrains.settings.AwsSettings
Expand All @@ -51,9 +49,6 @@ class DefaultToolkitAuthManagerTest {
batcher: TelemetryBatcher
) : TelemetryService(publisher, batcher)

@ExtendWith(MockRegionProviderExtension::class)
val regionProvider = MockRegionProviderRule()

@JvmField
@RegisterExtension
val mockClientManager = MockClientManagerExtension()
Expand All @@ -67,9 +62,13 @@ class DefaultToolkitAuthManagerTest {
@BeforeEach
fun setUp(@TestDisposable disposable: Disposable) {
mockClientManager.create<SsoOidcClient>()

sut = DefaultToolkitAuthManager()
ApplicationManager.getApplication().replaceService(ToolkitAuthManager::class.java, sut, disposable)
connectionManager = DefaultToolkitConnectionManager()

connectionManager = DefaultToolkitConnectionManager(projectRule.project)
projectRule.project.replaceService(ToolkitConnectionManager::class.java, connectionManager, disposable)

batcher = mock()
telemetryService = spy(TestTelemetryService(batcher = batcher))
ApplicationManager.getApplication().replaceService(TelemetryService::class.java, telemetryService, disposable)
Expand Down Expand Up @@ -222,12 +221,7 @@ class DefaultToolkitAuthManagerTest {
}

@Test
fun `loginSso with an working existing connection`(@TestDisposable disposable: Disposable) {
val connectionManager: ToolkitConnectionManager = mock()
regionProvider.addRegion(Region.US_EAST_1)
projectRule.project.replaceService(ToolkitConnectionManager::class.java, connectionManager, disposable)
ApplicationManager.getApplication().replaceService(ToolkitAuthManager::class.java, sut, disposable)

fun `loginSso with an working existing connection`() {
mockConstruction(InteractiveBearerTokenProvider::class.java) { context, _ ->
whenever(context.state()).thenReturn(BearerTokenAuthState.AUTHORIZED)
}.use {
Expand All @@ -248,12 +242,7 @@ class DefaultToolkitAuthManagerTest {
}

@Test
fun `loginSso with an existing connection but expired and refresh token is valid, should refreshToken`(@TestDisposable disposable: Disposable) {
val connectionManager = ToolkitConnectionManager.getInstance(projectRule.project)
regionProvider.addRegion(Region.US_EAST_1)
projectRule.project.replaceService(ToolkitConnectionManager::class.java, connectionManager, disposable)
ApplicationManager.getApplication().replaceService(ToolkitAuthManager::class.java, sut, disposable)

fun `loginSso with an existing connection but expired and refresh token is valid, should refreshToken`() {
mockConstruction(InteractiveBearerTokenProvider::class.java) { context, _ ->
whenever(context.id).thenReturn("id")
whenever(context.state()).thenReturn(BearerTokenAuthState.NEEDS_REFRESH)
Expand All @@ -276,13 +265,7 @@ class DefaultToolkitAuthManagerTest {
}

@Test
fun `loginSso with an existing connection that token is invalid and there's no refresh token, should re-authenticate`(
@TestDisposable disposable: Disposable
) {
val connectionManager = ToolkitConnectionManager.getInstance(projectRule.project)
regionProvider.addRegion(Region.US_EAST_1)
ApplicationManager.getApplication().replaceService(ToolkitAuthManager::class.java, sut, disposable)

fun `loginSso with an existing connection that token is invalid and there's no refresh token, should re-authenticate`() {
mockConstruction(InteractiveBearerTokenProvider::class.java) { context, _ ->
whenever(context.state()).thenReturn(BearerTokenAuthState.NOT_AUTHENTICATED)
}.use {
Expand All @@ -305,13 +288,12 @@ class DefaultToolkitAuthManagerTest {

@Test
fun `loginSso reuses connection if requested scopes are subset of existing`(@TestDisposable disposable: Disposable) {
val connectionManager = ToolkitConnectionManager.getInstance(projectRule.project)
regionProvider.addRegion(Region.US_EAST_1)
ApplicationManager.getApplication().replaceService(ToolkitAuthManager::class.java, sut, disposable)

mockConstruction(InteractiveBearerTokenProvider::class.java) { context, _ ->
whenever(context.state()).thenReturn(BearerTokenAuthState.AUTHORIZED)
}.use {
val connectionManager = spy(connectionManager)
projectRule.project.replaceService(ToolkitConnectionManager::class.java, connectionManager, disposable)

val existingConnection = sut.createConnection(
ManagedSsoProfile(
"us-east-1",
Expand All @@ -328,16 +310,12 @@ class DefaultToolkitAuthManagerTest {
verify(tokenProvider).state()
verifyNoMoreInteractions(tokenProvider)
assertThat(connectionManager.activeConnection()).isEqualTo(existingConnection)
verify(connectionManager, atLeastOnce()).switchConnection(existingConnection)
}
}

@Test
fun `loginSso forces reauth if requested scopes are not complete subset`(@TestDisposable disposable: Disposable) {
regionProvider.addRegion(Region.US_EAST_1)

projectRule.project.replaceService(ToolkitConnectionManager::class.java, connectionManager, disposable)
ApplicationManager.getApplication().replaceService(ToolkitAuthManager::class.java, sut, disposable)

fun `loginSso forces reauth if requested scopes are not complete subset`() {
mockConstruction(InteractiveBearerTokenProvider::class.java) { context, _ ->
whenever(context.state()).thenReturn(BearerTokenAuthState.AUTHORIZED)
}.use {
Expand All @@ -364,14 +342,12 @@ class DefaultToolkitAuthManagerTest {

@Test
fun `loginSso with a new connection`(@TestDisposable disposable: Disposable) {
val connectionManager: ToolkitConnectionManager = mock()
ApplicationManager.getApplication().replaceService(ToolkitAuthManager::class.java, sut, disposable)
projectRule.project.replaceService(ToolkitConnectionManager::class.java, connectionManager, disposable)

mockConstruction(InteractiveBearerTokenProvider::class.java) { context, _ ->
doNothing().whenever(context).reauthenticate()
whenever(context.state()).thenReturn(BearerTokenAuthState.NOT_AUTHENTICATED)
}.use {
val connectionManager = spy(connectionManager)
projectRule.project.replaceService(ToolkitConnectionManager::class.java, connectionManager, disposable)
// before
assertThat(sut.listConnections()).hasSize(0)

Expand Down Expand Up @@ -399,17 +375,14 @@ class DefaultToolkitAuthManagerTest {

@Test
fun `logoutFromConnection should invalidate the token provider and the connection and invoke callback`(@TestDisposable disposable: Disposable) {
regionProvider.addRegion(Region.US_EAST_1)
projectRule.project.replaceService(ToolkitConnectionManager::class.java, connectionManager, disposable)

val profile = ManagedSsoProfile("us-east-1", "startUrl000", listOf("scopes"))
val connection = ToolkitAuthManager.getInstance().createConnection(profile) as ManagedBearerSsoConnection
val connection = sut.createConnection(profile) as ManagedBearerSsoConnection
connectionManager.switchConnection(connection)

var providerInvalidatedMessageReceived = 0
var connectionSwitchedMessageReceived = 0
var callbackInvoked = 0
ApplicationManager.getApplication().messageBus.connect().subscribe(
ApplicationManager.getApplication().messageBus.connect(disposable).subscribe(
BearerTokenProviderListener.TOPIC,
object : BearerTokenProviderListener {
override fun invalidate(providerId: String) {
Expand All @@ -419,7 +392,7 @@ class DefaultToolkitAuthManagerTest {
}
}
)
ApplicationManager.getApplication().messageBus.connect().subscribe(
ApplicationManager.getApplication().messageBus.connect(disposable).subscribe(
ToolkitConnectionManagerListener.TOPIC,
object : ToolkitConnectionManagerListener {
override fun activeConnectionChanged(newConnection: ToolkitConnection?) {
Expand Down
Loading