Skip to content

Commit 617a451

Browse files
authored
feat(auth): warn if SSO user has no accounts #3713
Problem: If the user connects with a IdC/SSO account that is not fully configured, selecting the connection silently does nothing. Solution: Show a error message (unless it was already shown). Followup to #3704 24917d1
1 parent df7be50 commit 617a451

File tree

6 files changed

+49
-20
lines changed

6 files changed

+49
-20
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): show an error if SSO user is not assigned to an account with a Permission Set"
4+
}

src/auth/auth.ts

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { Timeout } from '../shared/utilities/timeoutUtils'
1717
import { errorCode, isAwsError, isNetworkError, ToolkitError, UnknownError } from '../shared/errors'
1818
import { getCache } from './sso/cache'
1919
import { createFactoryFunction, isNonNullable, Mutable } from '../shared/utilities/tsUtils'
20-
import { builderIdStartUrl, SsoToken } from './sso/model'
20+
import { builderIdStartUrl, SsoToken, truncateStartUrl } from './sso/model'
2121
import { SsoClient } from './sso/clients'
2222
import { getLogger } from '../shared/logger'
2323
import { CredentialsProviderManager } from './providers/credentialsProviderManager'
@@ -212,9 +212,10 @@ export class Auth implements AuthService, ConnectionManager {
212212
}
213213

214214
/**
215-
* Gathers all AWS accounts/roles associated with SSO ("IAM Identity Center", "IdC") connections.
215+
* Gathers all local profiles plus any AWS accounts/roles associated with SSO ("IAM Identity
216+
* Center", "IdC") connections.
216217
*
217-
* Use {@link Auth.listConnections} when you do not want to make extra API calls to the SSO service.
218+
* Use {@link Auth.listConnections} to avoid API calls to the SSO service.
218219
*/
219220
public listAndTraverseConnections(): AsyncCollection<Connection> {
220221
async function* load(this: Auth) {
@@ -233,13 +234,12 @@ export class Auth implements AuthService, ConnectionManager {
233234
.listProfiles()
234235
.filter(isLinkable)
235236
.map(([id, profile]) => {
236-
const startUrl = this.truncateStartUrl(profile.startUrl)
237237
return toCollection(() =>
238238
loadLinkedProfilesIntoStore(
239239
this.store,
240240
id,
241241
this.createSsoClient(profile.ssoRegion, this.getTokenProvider(id, profile)),
242-
startUrl
242+
profile.startUrl
243243
)
244244
)
245245
.catch(err => {
@@ -750,12 +750,8 @@ export class Auth implements AuthService, ConnectionManager {
750750
return (this.#instance ??= new Auth(new ProfileStore(memento)))
751751
}
752752

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

760756
return profile.startUrl === builderIdStartUrl
761757
? localizedText.builderId()

src/auth/connection.ts

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,18 @@
55
import * as vscode from 'vscode'
66
import { Credentials } from '@aws-sdk/types'
77
import { Mutable } from '../shared/utilities/tsUtils'
8-
import { builderIdStartUrl, SsoToken } from './sso/model'
8+
import { builderIdStartUrl, SsoToken, truncateStartUrl } from './sso/model'
99
import { SsoClient } from './sso/clients'
1010
import { CredentialsProviderManager } from './providers/credentialsProviderManager'
1111
import { fromString } from './providers/credentials'
1212
import { getLogger } from '../shared/logger/logger'
13+
import { showMessageWithUrl } from '../shared/utilities/messages'
14+
import { onceChanged } from '../shared/utilities/functionUtils'
15+
16+
/** Shows an error message unless it is the same as the last one shown. */
17+
const warnOnce = onceChanged((s: string, url: string) => {
18+
showMessageWithUrl(s, url, undefined, 'error')
19+
})
1320

1421
export const ssoScope = 'sso:account:access'
1522
export const codecatalystScopes = ['codecatalyst:read_write']
@@ -255,7 +262,7 @@ export async function* loadLinkedProfilesIntoStore(
255262
store: ProfileStore,
256263
source: SsoConnection['id'],
257264
client: SsoClient,
258-
profileLabel: string
265+
startUrl: string
259266
) {
260267
const accounts = new Set<string>()
261268
const found = new Set<Connection['id']>()
@@ -290,17 +297,21 @@ export async function* loadLinkedProfilesIntoStore(
290297
yield [id, profile] as const
291298
}
292299

293-
if (accounts.size === 0) {
300+
const isBuilderId = startUrl === builderIdStartUrl // Special case.
301+
if (!isBuilderId && (accounts.size === 0 || found.size === 0)) {
302+
const name = truncateStartUrl(startUrl)
294303
// Possible causes:
295304
// - SSO org has no "Permission sets"
296305
// - user is not an "Assigned user" in any account in the SSO org
297306
// - 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()
307+
if (accounts.size === 0) {
308+
getLogger().warn('auth: SSO org (%s) returned no accounts', name)
309+
} else if (found.size === 0) {
310+
getLogger().warn('auth: SSO org (%s) returned no roles for account: %s', name, Array.from(accounts).join())
311+
}
312+
warnOnce(
313+
`IAM Identity Center (${name}) returned no roles. Ensure the user is assigned to an account with a Permission Set.`,
314+
'https://docs.aws.amazon.com/singlesignon/latest/userguide/getting-started.html'
304315
)
305316
}
306317

src/auth/sso/model.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,10 @@ export const trustedDomainCancellation = 'TrustedDomainCancellation'
8282
const tryOpenHelpUrl = (url: vscode.Uri) =>
8383
openUrl(url).catch(e => getLogger().verbose('auth: failed to open help URL: %s', e))
8484

85+
export function truncateStartUrl(startUrl: string) {
86+
return startUrl.match(/https?:\/\/(.*)\.awsapps\.com\/start/)?.[1] ?? startUrl
87+
}
88+
8589
export async function openSsoPortalLink(
8690
startUrl: string,
8791
authorization: { readonly verificationUri: string; readonly userCode: string }

src/auth/ui/vue/show.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ export class AuthWebview extends VueWebview {
9191
}
9292

9393
/**
94-
* Returns true if any credentials are found, even ones associated with an sso
94+
* Returns true if any credentials are found, including those discovered from SSO service API.
9595
*/
9696
async isCredentialExists(): Promise<boolean> {
9797
return (await Auth.instance.listAndTraverseConnections().promise()).find(isIamConnection) !== undefined

src/test/credentials/auth.test.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,20 @@ describe('Auth', function () {
285285
)
286286
})
287287

288+
it('shows a user message if SSO connection returned no accounts/roles', async function () {
289+
auth.ssoClient.listAccounts.returns(
290+
toCollection(async function* () {
291+
yield []
292+
})
293+
)
294+
await auth.createConnection(linkedSsoProfile)
295+
await auth.listAndTraverseConnections().promise()
296+
assert.strictEqual(
297+
getTestWindow().shownMessages[0].message,
298+
'IAM Identity Center (d-0123456789) returned no roles. Ensure the user is assigned to an account with a Permission Set.'
299+
)
300+
})
301+
288302
it('does not gather linked accounts when calling `listConnections`', async function () {
289303
await auth.createConnection(linkedSsoProfile)
290304
const connections = await auth.listConnections()

0 commit comments

Comments
 (0)