Skip to content

Commit 24917d1

Browse files
authored
logging: warn if SSO user has no accounts #3704
Problem: If the user connects with a IdC/SSO account that is not fully configured, selecting the connection silently does nothing, and does not log anything. Solution: Log a warning.
1 parent d03b7d1 commit 24917d1

File tree

3 files changed

+43
-9
lines changed

3 files changed

+43
-9
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": "IAM Identity Center (SSO): log a warning if SSO user is not linked to an account"
4+
}

src/auth/auth.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ export class Auth implements AuthService, ConnectionManager {
212212
}
213213

214214
/**
215-
* This method will gather all AWS accounts/roles that are associated with SSO connections.
215+
* Gathers all AWS accounts/roles associated with SSO ("IAM Identity Center", "IdC") connections.
216216
*
217217
* Use {@link Auth.listConnections} when you do not want to make extra API calls to the SSO service.
218218
*/
@@ -232,20 +232,22 @@ export class Auth implements AuthService, ConnectionManager {
232232
const linked = this.store
233233
.listProfiles()
234234
.filter(isLinkable)
235-
.map(([id, profile]) =>
236-
toCollection(() =>
235+
.map(([id, profile]) => {
236+
const startUrl = this.truncateStartUrl(profile.startUrl)
237+
return toCollection(() =>
237238
loadLinkedProfilesIntoStore(
238239
this.store,
239240
id,
240-
this.createSsoClient(profile.ssoRegion, this.getTokenProvider(id, profile))
241+
this.createSsoClient(profile.ssoRegion, this.getTokenProvider(id, profile)),
242+
startUrl
241243
)
242244
)
243245
.catch(err => {
244246
getLogger().warn(`auth: failed to load linked profiles from "${id}": %s`, err)
245247
})
246248
.filter(isNonNullable)
247249
.map(entry => this.getConnectionFromStoreEntry(entry))
248-
)
250+
})
249251

250252
yield* linked.reduce(join, stream)
251253
}
@@ -748,8 +750,12 @@ export class Auth implements AuthService, ConnectionManager {
748750
return (this.#instance ??= new Auth(new ProfileStore(memento)))
749751
}
750752

753+
private truncateStartUrl(startUrl: string) {
754+
return startUrl.match(/https?:\/\/(.*)\.awsapps\.com\/start/)?.[1] ?? startUrl
755+
}
756+
751757
private getSsoProfileLabel(profile: SsoProfile) {
752-
const truncatedUrl = profile.startUrl.match(/https?:\/\/(.*)\.awsapps\.com\/start/)?.[1] ?? profile.startUrl
758+
const truncatedUrl = this.truncateStartUrl(profile.startUrl)
753759

754760
return profile.startUrl === builderIdStartUrl
755761
? localizedText.builderId()

src/auth/connection.ts

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { builderIdStartUrl, SsoToken } from './sso/model'
99
import { SsoClient } from './sso/clients'
1010
import { CredentialsProviderManager } from './providers/credentialsProviderManager'
1111
import { fromString } from './providers/credentials'
12+
import { getLogger } from '../shared/logger/logger'
1213

1314
export const ssoScope = 'sso:account:access'
1415
export const codecatalystScopes = ['codecatalyst:read_write']
@@ -247,18 +248,27 @@ export async function loadIamProfilesIntoStore(store: ProfileStore, manager: Cre
247248
}
248249
}
249250

251+
/**
252+
* Fetches profiles from the given SSO ("IAM Identity Center", "IdC") connection.
253+
*/
250254
export async function* loadLinkedProfilesIntoStore(
251255
store: ProfileStore,
252256
source: SsoConnection['id'],
253-
client: SsoClient
257+
client: SsoClient,
258+
profileLabel: string
254259
) {
260+
const accounts = new Set<string>()
261+
const found = new Set<Connection['id']>()
262+
255263
const stream = client
256264
.listAccounts()
257265
.flatten()
258-
.map(resp => client.listAccountRoles({ accountId: resp.accountId }).flatten())
266+
.map(resp => {
267+
accounts.add(resp.accountId)
268+
return client.listAccountRoles({ accountId: resp.accountId }).flatten()
269+
})
259270
.flatten()
260271

261-
const found = new Set<Connection['id']>()
262272
for await (const info of stream) {
263273
const name = `${info.roleName}-${info.accountId}`
264274
const id = `sso:${source}#${name}`
@@ -280,6 +290,20 @@ export async function* loadLinkedProfilesIntoStore(
280290
yield [id, profile] as const
281291
}
282292

293+
if (accounts.size === 0) {
294+
// Possible causes:
295+
// - SSO org has no "Permission sets"
296+
// - user is not an "Assigned user" in any account in the SSO org
297+
// - user is an "Assigned user" but no "Permission sets"
298+
getLogger().warn('auth: SSO org (%s) returned no accounts', profileLabel)
299+
} else if (found.size === 0) {
300+
getLogger().warn(
301+
'auth: SSO org (%s) returned no IAM credentials for account: %s',
302+
profileLabel,
303+
Array.from(accounts).join()
304+
)
305+
}
306+
283307
// Clean-up stale references in case the user no longer has access
284308
for (const [id, profile] of store.listProfiles()) {
285309
if (profile.type === 'iam' && profile.subtype === 'linked' && profile.ssoSession === source && !found.has(id)) {

0 commit comments

Comments
 (0)