Skip to content

Commit 06d1216

Browse files
authored
Limit the sso scopes to the service requesting it (#3528)
* Limit scopes to service calling it * new changes * feedback changes * try logout * fedback changes 2 * removed customizerId * feedback changes * variable name change * fixed tests * modified tests * detekt * fixed indent * added changelog
1 parent 9533511 commit 06d1216

File tree

9 files changed

+41
-32
lines changed

9 files changed

+41
-32
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"type" : "feature",
3+
"description" : "Using the least permissive set of scopes for features during BuilderID/SSO login. Using the same connection for multiple features will request additional scopes to be used."
4+
}

jetbrains-core/src/software/aws/toolkits/jetbrains/core/credentials/ToolkitAddConnectionDialog.kt

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import com.intellij.ui.dsl.builder.columns
2727
import com.intellij.ui.dsl.builder.panel
2828
import com.intellij.ui.dsl.builder.selected
2929
import com.intellij.ui.dsl.builder.toNullableProperty
30+
import com.intellij.util.containers.nullize
3031
import software.amazon.awssdk.services.ssooidc.model.InvalidGrantException
3132
import software.amazon.awssdk.services.ssooidc.model.InvalidRequestException
3233
import software.amazon.awssdk.services.ssooidc.model.SsoOidcException
@@ -36,8 +37,6 @@ import software.aws.toolkits.core.utils.getLogger
3637
import software.aws.toolkits.core.utils.info
3738
import software.aws.toolkits.core.utils.warn
3839
import software.aws.toolkits.jetbrains.ToolkitPlaces
39-
import software.aws.toolkits.jetbrains.core.credentials.sono.ALL_SONO_SCOPES
40-
import software.aws.toolkits.jetbrains.core.credentials.sono.ALL_SSO_SCOPES
4140
import software.aws.toolkits.jetbrains.core.credentials.sono.SONO_URL
4241
import software.aws.toolkits.jetbrains.core.help.HelpIds
4342
import software.aws.toolkits.jetbrains.core.region.AwsRegionProvider
@@ -52,6 +51,7 @@ data class ConnectionDialogCustomizer(
5251
val header: String? = null,
5352
val helpId: HelpIds? = null,
5453
val replaceIamComment: String? = null,
54+
val scopes: List<String>? = null,
5555
val startUrl: String? = null,
5656
val region: String? = null,
5757
val errorMsg: String? = null
@@ -148,11 +148,7 @@ open class ToolkitAddConnectionDialog(
148148
error("User should not perform Identity Center login with AWS Builder ID url")
149149
}
150150

151-
val scopes = if (loginType == LoginOptions.AWS_BUILDER_ID) {
152-
ALL_SONO_SCOPES
153-
} else {
154-
ALL_SSO_SCOPES
155-
}
151+
val scopes = customizer?.scopes?.nullize() ?: listOf("sso:account:access")
156152

157153
LOG.info {
158154
"""

jetbrains-core/src/software/aws/toolkits/jetbrains/core/credentials/ToolkitAuthManager.kt

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -103,15 +103,17 @@ interface ToolkitConnectionManager : Disposable {
103103
/**
104104
* Individual service should subscribe [ToolkitConnectionManagerListener.TOPIC] to fire their service activation / UX update
105105
*/
106-
fun loginSso(project: Project?, startUrl: String, region: String = DEFAULT_SSO_REGION, scopes: List<String> = ALL_SONO_SCOPES): BearerTokenProvider {
106+
107+
fun loginSso(project: Project?, startUrl: String, region: String = DEFAULT_SSO_REGION, requestedScopes: List<String> = ALL_SONO_SCOPES): BearerTokenProvider {
107108
val connectionId = ToolkitBearerTokenProvider.ssoIdentifier(startUrl, region)
108-
val manager = ToolkitAuthManager.getInstance()
109109

110+
val manager = ToolkitAuthManager.getInstance()
111+
val allScopes = requestedScopes.toMutableList()
110112
return manager.getConnection(connectionId)?.let { connection ->
111113
val logger = getLogger<ToolkitAuthManager>()
112114
// requested Builder ID, but one already exists
113115
// TBD: do we do this for regular SSO too?
114-
if (connection.isSono() && connection is BearerSsoConnection && scopes.all { it in connection.scopes }) {
116+
if (connection.isSono() && connection is BearerSsoConnection && requestedScopes.all { it in connection.scopes }) {
115117
val signOut = computeOnEdt {
116118
MessageDialogBuilder.yesNo(
117119
message("toolkit.login.aws_builder_id.already_connected.title"),
@@ -133,13 +135,17 @@ fun loginSso(project: Project?, startUrl: String, region: String = DEFAULT_SSO_R
133135
}
134136

135137
// There is an existing connection we can use
136-
if (connection is BearerSsoConnection && !scopes.all { it in connection.scopes }) {
138+
if (connection is BearerSsoConnection && !requestedScopes.all { it in connection.scopes }) {
139+
allScopes.addAll(connection.scopes)
140+
137141
logger.info {
138-
"Forcing reauth on ${connection.id} since requested scopes ($scopes) are not a complete subset of current scopes (${connection.scopes})"
142+
"""
143+
Forcing reauth on ${connection.id} since requested scopes ($requestedScopes)
144+
are not a complete subset of current scopes (${connection.scopes})
145+
""".trimIndent()
139146
}
140-
147+
logoutFromSsoConnection(project, connection as AwsBearerTokenConnection)
141148
// can't reuse since requested scopes are not in current connection. forcing reauth
142-
manager.deleteConnection(connection)
143149
return@let null
144150
}
145151

@@ -155,7 +161,7 @@ fun loginSso(project: Project?, startUrl: String, region: String = DEFAULT_SSO_R
155161
ManagedSsoProfile(
156162
region,
157163
startUrl,
158-
scopes
164+
allScopes.toSet().toList()
159165
)
160166
)
161167

jetbrains-core/src/software/aws/toolkits/jetbrains/core/credentials/sono/SonoConstants.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ internal val CODECATALYST_SCOPES = listOf(
1818

1919
// limit of 10
2020
// at least one scope must be provided
21-
internal val ALL_SSO_SCOPES = CODEWHISPERER_SCOPES
2221
val ALL_SONO_SCOPES = CODEWHISPERER_SCOPES + CODECATALYST_SCOPES
2322

2423
fun ToolkitConnection?.isSono() = if (this == null) {

jetbrains-core/src/software/aws/toolkits/jetbrains/core/credentials/sono/SonoCredentialManager.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,8 @@ class SonoCredentialManager {
6868
fun getProviderAndPromptAuth(): BearerTokenProvider {
6969
val provider = provider()
7070
return when (provider?.state()) {
71-
null -> runUnderProgressIfNeeded(project, message("credentials.sono.login.pending"), true) {
72-
loginSso(project, SONO_URL, scopes = ALL_SONO_SCOPES)
71+
null -> runUnderProgressIfNeeded(null, message("credentials.sono.login.pending"), true) {
72+
loginSso(project, SONO_URL, requestedScopes = CODECATALYST_SCOPES)
7373
}
7474

7575
else -> reauthProviderIfNeeded(project, provider)

jetbrains-core/src/software/aws/toolkits/jetbrains/services/codewhisperer/credentials/CodeWhispererLoginDialog.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package software.aws.toolkits.jetbrains.services.codewhisperer.credentials
66
import com.intellij.openapi.project.Project
77
import software.aws.toolkits.jetbrains.core.credentials.ConnectionDialogCustomizer
88
import software.aws.toolkits.jetbrains.core.credentials.ToolkitAddConnectionDialog
9+
import software.aws.toolkits.jetbrains.core.credentials.sono.CODEWHISPERER_SCOPES
910
import software.aws.toolkits.jetbrains.core.help.HelpIds
1011
import software.aws.toolkits.resources.message
1112

@@ -16,6 +17,7 @@ class CodeWhispererLoginDialog(project: Project) : ToolkitAddConnectionDialog(
1617
title = message("codewhisperer.credential.login.dialog.title"),
1718
header = message("codewhisperer.credential.login.dialog.prompt"),
1819
helpId = HelpIds.CODEWHISPERER_LOGIN_DIALOG,
19-
replaceIamComment = message("codewhisperer.credential.login.dialog.iam.description")
20+
replaceIamComment = message("codewhisperer.credential.login.dialog.iam.description"),
21+
scopes = CODEWHISPERER_SCOPES
2022
)
2123
)

jetbrains-core/src/software/aws/toolkits/jetbrains/services/codewhisperer/util/CodeWhispererUtil.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ object CodeWhispererUtil {
164164
if (connection !is BearerSsoConnection) return
165165
ApplicationManager.getApplication().executeOnPooledThread {
166166
getConnectionStartUrl(connection)?.let { startUrl ->
167-
loginSso(project, startUrl, scopes = connection.scopes)
167+
loginSso(project, startUrl, requestedScopes = connection.scopes)
168168
}
169169
}
170170
}

jetbrains-core/tst/software/aws/toolkits/jetbrains/core/credentials/DefaultToolkitAuthManagerTest.kt

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import org.mockito.kotlin.argumentCaptor
1818
import org.mockito.kotlin.doNothing
1919
import org.mockito.kotlin.eq
2020
import org.mockito.kotlin.mock
21+
import org.mockito.kotlin.times
2122
import org.mockito.kotlin.verify
2223
import org.mockito.kotlin.verifyNoMoreInteractions
2324
import org.mockito.kotlin.whenever
@@ -160,7 +161,7 @@ class DefaultToolkitAuthManagerTest {
160161
)
161162
)
162163

163-
loginSso(projectRule.project, "foo", scopes = emptyList())
164+
loginSso(projectRule.project, "foo", requestedScopes = emptyList())
164165

165166
val tokenProvider = it.constructed()[0]
166167
verify(tokenProvider).state()
@@ -188,7 +189,7 @@ class DefaultToolkitAuthManagerTest {
188189
)
189190
)
190191

191-
loginSso(projectRule.project, "foo", scopes = emptyList())
192+
loginSso(projectRule.project, "foo", requestedScopes = emptyList())
192193

193194
val tokenProvider = it.constructed()[0]
194195
verify(tokenProvider).resolveToken()
@@ -214,7 +215,7 @@ class DefaultToolkitAuthManagerTest {
214215
)
215216
)
216217

217-
loginSso(projectRule.project, "foo", scopes = emptyList())
218+
loginSso(projectRule.project, "foo", requestedScopes = emptyList())
218219

219220
val tokenProvider = it.constructed()[0]
220221
verify(tokenProvider).reauthenticate()
@@ -240,7 +241,7 @@ class DefaultToolkitAuthManagerTest {
240241
)
241242
)
242243

243-
loginSso(projectRule.project, "foo", scopes = listOf("existing1"))
244+
loginSso(projectRule.project, "foo", requestedScopes = listOf("existing1"))
244245

245246
val tokenProvider = it.constructed()[0]
246247
verify(tokenProvider).state()
@@ -268,17 +269,18 @@ class DefaultToolkitAuthManagerTest {
268269
)
269270

270271
val newScopes = listOf("existing1", "new1")
271-
loginSso(projectRule.project, "foo", scopes = newScopes)
272+
loginSso(projectRule.project, "foo", requestedScopes = newScopes)
272273

273274
val captor = argumentCaptor<ManagedBearerSsoConnection>()
274-
verify(connectionManager).switchConnection(captor.capture())
275-
assertThat(captor.allValues.size).isEqualTo(1)
276-
assertThat(captor.firstValue).satisfies { connection ->
277-
assertThat(connection.scopes).usingRecursiveComparison().isEqualTo(newScopes)
275+
276+
verify(connectionManager, times(2)).switchConnection(captor.capture())
277+
278+
assertThat(captor.secondValue).satisfies { connection ->
279+
assertThat(connection.scopes.toSet()).isEqualTo(setOf("existing1", "existing2", "existing3", "new1"))
278280
}
279281
assertThat(sut.listConnections()).singleElement().isInstanceOfSatisfying<BearerSsoConnection>() { connection ->
280282
assertThat(connection).usingRecursiveComparison().isNotEqualTo(existingConnection)
281-
assertThat(connection.scopes).usingRecursiveComparison().isEqualTo(newScopes)
283+
assertThat(connection.scopes.toSet()).isEqualTo(setOf("existing1", "existing2", "existing3", "new1"))
282284
}
283285
}
284286
}
@@ -296,7 +298,7 @@ class DefaultToolkitAuthManagerTest {
296298
// before
297299
assertThat(sut.listConnections()).hasSize(0)
298300

299-
loginSso(projectRule.project, "foo", scopes = listOf("scope1", "scope2"))
301+
loginSso(projectRule.project, "foo", requestedScopes = listOf("scope1", "scope2"))
300302

301303
// after
302304
assertThat(sut.listConnections()).hasSize(1)

resources/resources/software/aws/toolkits/resources/MessagesBundle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -454,7 +454,7 @@ codewhisperer.credential.login.dialog.title=CodeWhisperer: Add Connection to AWS
454454
codewhisperer.credential.login.exception.general=Ran into unknown error: {0}
455455
codewhisperer.credential.login.exception.general.oidc=Fail to fetch credential
456456
codewhisperer.credential.login.exception.invalid_grant=Access denied
457-
codewhisperer.credential.login.exception.invalid_input=Invalid start url
457+
codewhisperer.credential.login.exception.invalid_input=Invalid start url or scopes
458458
codewhisperer.credential.login.exception.io=Unable to access SSO disk cache: {0}
459459
codewhisperer.credential.login.prompt.sono.message=Sign in with AWS Builder ID
460460
codewhisperer.credential.login.prompt.sono.title=AWS Builder ID login

0 commit comments

Comments
 (0)