Skip to content

Commit ccbdc7c

Browse files
authored
fix: misleading "Permission set" warning for OIDC SSO/IdC connection #3748
Problem: Some IdC/SSO connections have OIDC "scopes" and don't have IAM "roles" (returning IAM credentials / "Permission sets"). Such connections are useful for scope-based applications, even though they don't have IAM credentials. Solution: Don't warn about missing roles/permission-sets for connections that have OIDC scopes.
1 parent e619005 commit ccbdc7c

File tree

4 files changed

+36
-23
lines changed

4 files changed

+36
-23
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"type": "Bug Fix",
3+
"description": "IAM Identity Center (SSO): misleading \"Permission set\" warning for scope-based SSO/IdC connection"
4+
}

src/auth/auth.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -223,12 +223,16 @@ export class Auth implements AuthService, ConnectionManager {
223223

224224
const stream = toStream(this.store.listProfiles().map(entry => this.getConnectionFromStoreEntry(entry)))
225225

226+
/** Decides if SSO service should be queried for "linked" IAM roles/credentials for the given SSO connection. */
226227
const isLinkable = (
227228
entry: [string, StoredProfile<Profile>]
228-
): entry is [string, StoredProfile<SsoProfile>] =>
229-
entry[1].type === 'sso' &&
230-
hasScopes(entry[1], ssoAccountAccessScopes) &&
231-
entry[1].metadata.connectionState === 'valid'
229+
): entry is [string, StoredProfile<SsoProfile>] => {
230+
const r =
231+
entry[1].type === 'sso' &&
232+
hasScopes(entry[1], ssoAccountAccessScopes) &&
233+
entry[1].metadata.connectionState === 'valid'
234+
return r
235+
}
232236

233237
const linked = this.store
234238
.listProfiles()
@@ -238,8 +242,8 @@ export class Auth implements AuthService, ConnectionManager {
238242
loadLinkedProfilesIntoStore(
239243
this.store,
240244
id,
241-
this.createSsoClient(profile.ssoRegion, this.getTokenProvider(id, profile)),
242-
profile.startUrl
245+
profile,
246+
this.createSsoClient(profile.ssoRegion, this.getTokenProvider(id, profile))
243247
)
244248
)
245249
.catch(err => {

src/auth/connection.ts

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ const warnOnce = onceChanged((s: string, url: string) => {
1818
showMessageWithUrl(s, url, undefined, 'error')
1919
})
2020

21-
export const ssoScope = 'sso:account:access'
2221
export const codecatalystScopes = ['codecatalyst:read_write']
2322
export const ssoAccountAccessScopes = ['sso:account:access']
2423
export const codewhispererScopes = ['codewhisperer:completions', 'codewhisperer:analysis']
@@ -256,13 +255,14 @@ export async function loadIamProfilesIntoStore(store: ProfileStore, manager: Cre
256255
}
257256

258257
/**
259-
* Fetches profiles from the given SSO ("IAM Identity Center", "IdC") connection.
258+
* Gets credentials profiles constructed from roles ("Permission Sets") discovered from the given
259+
* SSO ("IAM Identity Center", "IdC") connection.
260260
*/
261261
export async function* loadLinkedProfilesIntoStore(
262262
store: ProfileStore,
263-
source: SsoConnection['id'],
264-
client: SsoClient,
265-
startUrl: string
263+
sourceId: SsoConnection['id'],
264+
ssoProfile: StoredProfile<SsoProfile>,
265+
client: SsoClient
266266
) {
267267
const accounts = new Set<string>()
268268
const found = new Set<Connection['id']>()
@@ -278,7 +278,7 @@ export async function* loadLinkedProfilesIntoStore(
278278

279279
for await (const info of stream) {
280280
const name = `${info.roleName}-${info.accountId}`
281-
const id = `sso:${source}#${name}`
281+
const id = `sso:${sourceId}#${name}`
282282
found.add(id)
283283

284284
if (store.getProfile(id) !== undefined) {
@@ -289,21 +289,21 @@ export async function* loadLinkedProfilesIntoStore(
289289
name,
290290
type: 'iam',
291291
subtype: 'linked',
292-
ssoSession: source,
292+
ssoSession: sourceId,
293293
ssoRoleName: info.roleName,
294294
ssoAccountId: info.accountId,
295295
})
296296

297297
yield [id, profile] as const
298298
}
299299

300-
const isBuilderId = startUrl === builderIdStartUrl // Special case.
301-
if (!isBuilderId && (accounts.size === 0 || found.size === 0)) {
302-
const name = truncateStartUrl(startUrl)
303-
// Possible causes:
304-
// - SSO org has no "Permission sets"
300+
/** Does `ssoProfile` have scopes other than "sso:account:access"? */
301+
const hasScopes = !!ssoProfile.scopes?.some(s => !ssoAccountAccessScopes.includes(s))
302+
if (!hasScopes && (accounts.size === 0 || found.size === 0)) {
303+
// SSO user has no OIDC scopes nor IAM roles. Possible causes:
305304
// - user is not an "Assigned user" in any account in the SSO org
306-
// - user is an "Assigned user" but no "Permission sets"
305+
// - SSO org has no "Permission sets"
306+
const name = truncateStartUrl(ssoProfile.startUrl)
307307
if (accounts.size === 0) {
308308
getLogger().warn('auth: SSO org (%s) returned no accounts', name)
309309
} else if (found.size === 0) {
@@ -317,7 +317,12 @@ export async function* loadLinkedProfilesIntoStore(
317317

318318
// Clean-up stale references in case the user no longer has access
319319
for (const [id, profile] of store.listProfiles()) {
320-
if (profile.type === 'iam' && profile.subtype === 'linked' && profile.ssoSession === source && !found.has(id)) {
320+
if (
321+
profile.type === 'iam' &&
322+
profile.subtype === 'linked' &&
323+
profile.ssoSession === sourceId &&
324+
!found.has(id)
325+
) {
321326
await store.deleteProfile(id)
322327
}
323328
}

src/auth/providers/sharedCredentialsProvider.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import {
3131
} from '../credentials/sharedCredentials'
3232
import { builderIdStartUrl } from '../sso/model'
3333
import { SectionName, SharedCredentialsKeys } from '../credentials/types'
34-
import { SsoProfile, hasScopes } from '../connection'
34+
import { SsoProfile, hasScopes, ssoAccountAccessScopes } from '../connection'
3535

3636
const credentialSources = {
3737
ECS_CONTAINER: 'EcsContainer',
@@ -150,7 +150,7 @@ export class SharedCredentialsProvider implements CredentialsProvider {
150150

151151
return {
152152
type: 'sso',
153-
scopes: ['sso:account:access'],
153+
scopes: ssoAccountAccessScopes,
154154
startUrl: this.profile[SharedCredentialsKeys.SSO_START_URL],
155155
ssoRegion: this.profile[SharedCredentialsKeys.SSO_REGION] ?? defaultRegion,
156156
}
@@ -348,7 +348,7 @@ export class SharedCredentialsProvider implements CredentialsProvider {
348348

349349
private makeSsoCredentaislProvider() {
350350
const ssoProfile = this.getSsoProfileFromProfile()
351-
if (!hasScopes(ssoProfile, ['sso:account:access'])) {
351+
if (!hasScopes(ssoProfile, ssoAccountAccessScopes)) {
352352
throw new Error(`Session for "${this.profileName}" is missing required scope: sso:account:access`)
353353
}
354354

0 commit comments

Comments
 (0)