Skip to content

Commit bf612d8

Browse files
authored
feat(auth): add support for sso-session (#3317)
## Problem Users are unable to use SSO configurations created by newer versions of the AWS CLI. For example, `aws configure sso` might produce something like this: ```ini [profile default] sso_session = default sso_account_id = 012345678910 sso_role_name = MyRole [sso-session default] sso_region = us-east-1 sso_start_url = https://d-xxxxxxxxx.awsapps.com/start sso_registration_scopes = sso:account:access ``` Users should be able to select the `default` profile in the Toolkit. ## Solution Support `sso-session` sections and the corresponding `sso_session` property. This PR does not surface SSO sections in the UI. Only profiles are valid options.
1 parent 4796b94 commit bf612d8

14 files changed

+635
-452
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": "auth: support `sso_session` for profiles in AWS shared ini files"
4+
}

src/credentials/credentialsUtilities.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,12 @@ const localize = nls.loadMessageBundle()
99
import * as vscode from 'vscode'
1010
import { Credentials } from '@aws-sdk/types'
1111
import { authHelpUrl } from '../shared/constants'
12-
import { Profile } from '../shared/credentials/credentialsFile'
1312
import globals from '../shared/extensionGlobals'
1413
import { isCloud9 } from '../shared/extensionUtilities'
1514
import { messages, showMessageWithCancel, showViewLogsMessage } from '../shared/utilities/messages'
1615
import { Timeout, waitTimeout } from '../shared/utilities/timeoutUtils'
1716
import { fromExtensionManifest } from '../shared/settings'
17+
import { Profile } from './sharedCredentials'
1818

1919
const credentialsTimeout = 300000 // 5 minutes
2020
const credentialsProgressDelay = 1000

src/credentials/providers/sharedCredentialsProvider.ts

Lines changed: 88 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,27 @@ import { ParsedIniData, SharedConfigFiles } from '@aws-sdk/shared-ini-file-loade
1010
import { chain } from '@aws-sdk/property-provider'
1111
import { fromInstanceMetadata, fromContainerMetadata } from '@aws-sdk/credential-provider-imds'
1212
import { fromEnv } from '@aws-sdk/credential-provider-env'
13-
import { Profile } from '../../shared/credentials/credentialsFile'
1413
import { getLogger } from '../../shared/logger'
1514
import { getStringHash } from '../../shared/utilities/textUtilities'
1615
import { getMfaTokenFromUser } from '../credentialsCreator'
1716
import { resolveProviderWithCancel } from '../credentialsUtilities'
1817
import { CredentialsProvider, CredentialsProviderType, CredentialsId } from './credentials'
1918
import { CredentialType } from '../../shared/telemetry/telemetry.gen'
20-
import { getMissingProps, hasProps } from '../../shared/utilities/tsUtils'
19+
import { assertHasProps, getMissingProps, hasProps } from '../../shared/utilities/tsUtils'
2120
import { DefaultStsClient } from '../../shared/clients/stsClient'
2221
import { SsoAccessTokenProvider } from '../sso/ssoAccessTokenProvider'
2322
import { SsoClient } from '../sso/clients'
2423
import { toRecord } from '../../shared/utilities/collectionUtils'
24+
import {
25+
extractData,
26+
getRequiredFields,
27+
getSectionDataOrThrow,
28+
getSectionOrThrow,
29+
isProfileSection,
30+
Profile,
31+
Section,
32+
} from '../sharedCredentials'
33+
import { hasScopes, SsoProfile } from '../auth'
2534

2635
const sharedCredentialProperties = {
2736
AWS_ACCESS_KEY_ID: 'aws_access_key_id',
@@ -37,6 +46,8 @@ const sharedCredentialProperties = {
3746
SSO_REGION: 'sso_region',
3847
SSO_ACCOUNT_ID: 'sso_account_id',
3948
SSO_ROLE_NAME: 'sso_role_name',
49+
SSO_SESSION: 'sso_session',
50+
SSO_REGISTRATION_SCOPES: 'sso_registration_scopes',
4051
} as const
4152

4253
const credentialSources = {
@@ -55,6 +66,7 @@ function validateProfile(profile: Profile, ...props: string[]): string | undefin
5566

5667
function isSsoProfile(profile: Profile): boolean {
5768
return (
69+
hasProps(profile, sharedCredentialProperties.SSO_SESSION) ||
5870
hasProps(profile, sharedCredentialProperties.SSO_START_URL) ||
5971
hasProps(profile, sharedCredentialProperties.SSO_REGION) ||
6072
hasProps(profile, sharedCredentialProperties.SSO_ROLE_NAME) ||
@@ -66,20 +78,10 @@ function isSsoProfile(profile: Profile): boolean {
6678
* Represents one profile from the AWS Shared Credentials files.
6779
*/
6880
export class SharedCredentialsProvider implements CredentialsProvider {
69-
private readonly profile: Profile
81+
private readonly section = getSectionOrThrow(this.sections, this.profileName, 'profile')
82+
private readonly profile = extractData(this.section)
7083

71-
public constructor(
72-
private readonly profileName: string,
73-
private readonly allSharedCredentialProfiles: Map<string, Profile>
74-
) {
75-
const profile = this.allSharedCredentialProfiles.get(profileName)
76-
77-
if (!profile) {
78-
throw new Error(`Profile not found: ${profileName}`)
79-
}
80-
81-
this.profile = profile
82-
}
84+
public constructor(private readonly profileName: string, private readonly sections: Section[]) {}
8385

8486
public getCredentialsId(): CredentialsId {
8587
return {
@@ -139,6 +141,37 @@ export class SharedCredentialsProvider implements CredentialsProvider {
139141
return true
140142
}
141143

144+
private getProfile(name: string) {
145+
return getSectionDataOrThrow(this.sections, name, 'profile')
146+
}
147+
148+
private getSsoProfileFromProfile(): SsoProfile & { identifier?: string } {
149+
const defaultRegion = this.getDefaultRegion() ?? 'us-east-1'
150+
const sessionName = this.profile[sharedCredentialProperties.SSO_SESSION]
151+
if (sessionName === undefined) {
152+
assertHasProps(this.profile, sharedCredentialProperties.SSO_START_URL)
153+
154+
return {
155+
type: 'sso',
156+
scopes: ['sso:account:access'],
157+
startUrl: this.profile[sharedCredentialProperties.SSO_START_URL],
158+
ssoRegion: this.profile[sharedCredentialProperties.SSO_REGION] ?? defaultRegion,
159+
}
160+
}
161+
162+
const sessionData = getSectionDataOrThrow(this.sections, sessionName, 'sso-session')
163+
const scopes = sessionData[sharedCredentialProperties.SSO_REGISTRATION_SCOPES]
164+
assertHasProps(sessionData, sharedCredentialProperties.SSO_START_URL)
165+
166+
return {
167+
type: 'sso',
168+
identifier: sessionName,
169+
scopes: scopes?.split(',').map(s => s.trim()),
170+
startUrl: sessionData[sharedCredentialProperties.SSO_START_URL],
171+
ssoRegion: sessionData[sharedCredentialProperties.SSO_REGION] ?? defaultRegion,
172+
}
173+
}
174+
142175
/**
143176
* Returns undefined if the Profile is valid, else a string indicating what is invalid
144177
*/
@@ -161,13 +194,7 @@ export class SharedCredentialsProvider implements CredentialsProvider {
161194
sharedCredentialProperties.AWS_SECRET_ACCESS_KEY
162195
)
163196
} else if (isSsoProfile(this.profile)) {
164-
return validateProfile(
165-
this.profile,
166-
sharedCredentialProperties.SSO_START_URL,
167-
sharedCredentialProperties.SSO_REGION,
168-
sharedCredentialProperties.SSO_ROLE_NAME,
169-
sharedCredentialProperties.SSO_ACCOUNT_ID
170-
)
197+
return undefined
171198
} else {
172199
return 'not supported by the Toolkit'
173200
}
@@ -191,7 +218,7 @@ export class SharedCredentialsProvider implements CredentialsProvider {
191218

192219
const source = new SharedCredentialsProvider(
193220
this.profile[sharedCredentialProperties.SOURCE_PROFILE]!,
194-
this.allSharedCredentialProfiles
221+
this.sections
195222
)
196223
const creds = await source.getCredentials()
197224
loadedCreds[this.profile[sharedCredentialProperties.SOURCE_PROFILE]!] = {
@@ -252,13 +279,13 @@ export class SharedCredentialsProvider implements CredentialsProvider {
252279
profilesTraversed.push(profileName)
253280

254281
// Missing reference
255-
if (!this.allSharedCredentialProfiles.has(profileName)) {
282+
if (!this.sections.find(s => s.name === profileName && s.type === 'profile')) {
256283
return `Shared Credentials Profile ${profileName} not found. Reference chain: ${profilesTraversed.join(
257284
' -> '
258285
)}`
259286
}
260287

261-
profile = this.allSharedCredentialProfiles.get(profileName)!
288+
profile = this.getProfile(profileName)
262289
}
263290
}
264291

@@ -315,31 +342,44 @@ export class SharedCredentialsProvider implements CredentialsProvider {
315342
return this.makeSharedIniFileCredentialsProvider(loadedCreds)
316343
}
317344

318-
if (hasProps(this.profile, sharedCredentialProperties.SSO_START_URL)) {
319-
logger.verbose(
320-
`Profile ${this.profileName} contains ${sharedCredentialProperties.SSO_START_URL} - treating as SSO Credentials`
321-
)
322-
323-
const region = this.profile[sharedCredentialProperties.SSO_REGION]!
324-
const startUrl = this.profile[sharedCredentialProperties.SSO_START_URL]!
325-
const accountId = this.profile[sharedCredentialProperties.SSO_ACCOUNT_ID]!
326-
const roleName = this.profile[sharedCredentialProperties.SSO_ROLE_NAME]!
327-
const tokenProvider = new SsoAccessTokenProvider({ region, startUrl })
328-
const client = SsoClient.create(region, tokenProvider)
329-
330-
return async () => {
331-
if ((await tokenProvider.getToken()) === undefined) {
332-
await tokenProvider.createToken()
333-
}
345+
if (isSsoProfile(this.profile)) {
346+
logger.verbose(`Profile ${this.profileName} is an SSO profile - treating as SSO Credentials`)
334347

335-
return client.getRoleCredentials({ accountId, roleName })
336-
}
348+
return this.makeSsoCredentaislProvider()
337349
}
338350

339351
logger.error(`Profile ${this.profileName} did not contain any supported properties`)
340352
throw new Error(`Shared Credentials profile ${this.profileName} is not supported`)
341353
}
342354

355+
private makeSsoCredentaislProvider() {
356+
const ssoProfile = this.getSsoProfileFromProfile()
357+
if (!hasScopes(ssoProfile, ['sso:account:access'])) {
358+
throw new Error(`Session for "${this.profileName}" is missing required scope: sso:account:access`)
359+
}
360+
361+
const region = ssoProfile.ssoRegion
362+
const tokenProvider = new SsoAccessTokenProvider({ ...ssoProfile, region })
363+
const client = SsoClient.create(region, tokenProvider)
364+
365+
return async () => {
366+
if ((await tokenProvider.getToken()) === undefined) {
367+
await tokenProvider.createToken()
368+
}
369+
370+
const data = getRequiredFields(
371+
this.section,
372+
sharedCredentialProperties.SSO_ACCOUNT_ID,
373+
sharedCredentialProperties.SSO_ROLE_NAME
374+
)
375+
376+
return client.getRoleCredentials({
377+
accountId: data[sharedCredentialProperties.SSO_ACCOUNT_ID],
378+
roleName: data[sharedCredentialProperties.SSO_ROLE_NAME],
379+
})
380+
}
381+
}
382+
343383
private makeSharedIniFileCredentialsProvider(loadedCreds?: ParsedIniData): AWS.CredentialProvider {
344384
const assumeRole = async (credentials: AWS.Credentials, params: AssumeRoleParams) => {
345385
const region = this.getDefaultRegion() ?? 'us-east-1'
@@ -356,7 +396,11 @@ export class SharedCredentialsProvider implements CredentialsProvider {
356396
// Our credentials logic merges profiles from the credentials and config files but SDK v3 does not
357397
// This can cause odd behavior where the Toolkit can switch to a profile but not authenticate with it
358398
// So the workaround is to do give the SDK the merged profiles directly
359-
const profiles = toRecord(this.allSharedCredentialProfiles.keys(), k => this.allSharedCredentialProfiles.get(k))
399+
const profileSections = this.sections.filter(isProfileSection)
400+
const profiles = toRecord(
401+
profileSections.map(s => s.name),
402+
k => this.getProfile(k)
403+
)
360404

361405
return fromIni({
362406
profile: this.profileName,

src/credentials/providers/sharedCredentialsProviderFactory.ts

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { getLogger, Logger } from '../../shared/logger'
88
import {
99
getConfigFilename,
1010
getCredentialsFilename,
11-
loadSharedCredentialsProfiles,
11+
loadSharedCredentialsSections,
1212
updateAwsSdkLoadConfigEnvVar,
1313
} from '../sharedCredentials'
1414
import { CredentialsProviderType } from './credentials'
@@ -51,24 +51,25 @@ export class SharedCredentialsProviderFactory extends BaseCredentialsProviderFac
5151
private async loadSharedCredentialsProviders(): Promise<void> {
5252
this.resetProviders()
5353

54-
this.logger.verbose('Loading all Shared Credentials Profiles')
55-
const allCredentialProfiles = await loadSharedCredentialsProfiles()
54+
this.logger.verbose('Loading all Shared Credentials Sections')
55+
const result = await loadSharedCredentialsSections()
56+
if (result.errors.length > 0) {
57+
const errors = result.errors.map(e => e.message).join('\t\n')
58+
getLogger().verbose(`credentials: errors occurred while parsing:\n%s`, errors)
59+
}
60+
5661
this.loadedCredentialsModificationMillis = await this.getLastModifiedMillis(getCredentialsFilename())
5762
this.loadedConfigModificationMillis = await this.getLastModifiedMillis(getConfigFilename())
5863
await updateAwsSdkLoadConfigEnvVar()
5964

60-
const profileNames = Array.from(allCredentialProfiles.keys())
61-
getLogger().verbose(`credentials: found profiles: ${profileNames}`)
62-
for (const profileName of profileNames) {
63-
const profile = allCredentialProfiles.get(profileName)
64-
if (!profile) {
65-
continue
65+
getLogger().verbose(`credentials: found sections: ${result.sections.map(s => `${s.type}:${s.name}`)}`)
66+
for (const section of result.sections) {
67+
if (section.type === 'profile') {
68+
await this.addProviderIfValid(
69+
section.name,
70+
new SharedCredentialsProvider(section.name, result.sections)
71+
)
6672
}
67-
68-
await this.addProviderIfValid(
69-
profileName,
70-
new SharedCredentialsProvider(profileName, allCredentialProfiles)
71-
)
7273
}
7374
}
7475

0 commit comments

Comments
 (0)