Skip to content

Commit d232fc4

Browse files
Merge master into feature/web
2 parents 30cabf9 + 0fbca15 commit d232fc4

File tree

16 files changed

+461
-68
lines changed

16 files changed

+461
-68
lines changed

src/auth/auth.ts

Lines changed: 57 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ import { CredentialsProviderManager } from './providers/credentialsProviderManag
2424
import { asString, CredentialsId, CredentialsProvider, fromString } from './providers/credentials'
2525
import { once } from '../shared/utilities/functionUtils'
2626
import { CredentialsSettings } from './credentials/utils'
27+
import {
28+
extractDataFromSection,
29+
getSectionOrThrow,
30+
loadSharedCredentialsSections,
31+
} from './credentials/sharedCredentials'
2732
import { getCodeCatalystDevEnvId } from '../shared/vscode/env'
2833
import { partition } from '../shared/utilities/mementos'
2934
import { SsoCredentialsProvider } from './providers/ssoCredentialsProvider'
@@ -47,8 +52,9 @@ import {
4752
StoredProfile,
4853
codecatalystScopes,
4954
createBuilderIdProfile,
55+
createSsoProfile,
5056
hasScopes,
51-
isBuilderIdConnection,
57+
isValidCodeCatalystConnection,
5258
loadIamProfilesIntoStore,
5359
loadLinkedProfilesIntoStore,
5460
ssoAccountAccessScopes,
@@ -521,9 +527,11 @@ export class Auth implements AuthService, ConnectionManager {
521527
}
522528

523529
// XXX: always read from the same location in a dev environment
524-
private getSsoSessionName = once(() => {
530+
// This detection is fuzzy if an sso-session section exists for any other reason.
531+
private detectSsoSessionNameForCodeCatalyst = once((): string => {
525532
try {
526533
const configFile = getConfigFilename()
534+
// `require('fs')` is workaround for web mode:
527535
const contents: string = require('fs').readFileSync(configFile, 'utf-8')
528536
const identifier = contents.match(/\[sso\-session (.*)\]/)?.[1]
529537
if (!identifier) {
@@ -532,21 +540,40 @@ export class Auth implements AuthService, ConnectionManager {
532540

533541
return identifier
534542
} catch (err) {
535-
const defaultName = 'codecatalyst'
536-
getLogger().warn(`auth: unable to get an sso session name, defaulting to "${defaultName}": %s`, err)
537-
538-
return defaultName
543+
const identifier = 'codecatalyst'
544+
getLogger().warn(`auth: unable to get an sso session name, defaulting to "${identifier}": %s`, err)
545+
return identifier
539546
}
540547
})
541548

549+
private createCodeCatalystDevEnvProfile = async (): Promise<[id: string, profile: SsoProfile]> => {
550+
const identifier = this.detectSsoSessionNameForCodeCatalyst()
551+
552+
const { sections } = await loadSharedCredentialsSections()
553+
const { sso_region: region, sso_start_url: startUrl } = extractDataFromSection(
554+
getSectionOrThrow(sections, identifier, 'sso-session')
555+
)
556+
557+
if ([region, startUrl].some(prop => typeof prop !== 'string')) {
558+
throw new ToolkitError('sso-session data missing in ~/.aws/config', { code: 'NoSsoSession' })
559+
}
560+
561+
return startUrl === builderIdStartUrl
562+
? [identifier, createBuilderIdProfile(codecatalystScopes)]
563+
: [identifier, createSsoProfile(region, startUrl, codecatalystScopes)]
564+
}
565+
542566
private getTokenProvider(id: Connection['id'], profile: StoredProfile<SsoProfile>) {
543-
// XXX: Use the token created by dev environments if and only if the profile is strictly for CodeCatalyst
567+
// XXX: Use the token created by Dev Environments if and only if the profile is strictly
568+
// for CodeCatalyst, as indicated by its set of scopes. A consequence of these semantics is
569+
// that any profile will be coerced to use this token if that profile exclusively contains
570+
// CodeCatalyst scopes. Similarly, if additional scopes are added to a profile, the profile
571+
// no longer matches this condition.
544572
const shouldUseSoftwareStatement =
545573
getCodeCatalystDevEnvId() !== undefined &&
546-
profile.startUrl === builderIdStartUrl &&
547574
profile.scopes?.every(scope => codecatalystScopes.includes(scope))
548575

549-
const tokenIdentifier = shouldUseSoftwareStatement ? this.getSsoSessionName() : id
576+
const tokenIdentifier = shouldUseSoftwareStatement ? this.detectSsoSessionNameForCodeCatalyst() : id
550577

551578
return this.createTokenProvider(
552579
{
@@ -702,15 +729,28 @@ export class Auth implements AuthService, ConnectionManager {
702729
})
703730
)
704731

705-
// Use the environment token if available
706-
// This token only has CC permissions currently!
732+
// When opening a Dev Environment, use the environment token if no other CodeCatalyst
733+
// credential is in use. This token only has CC permissions currently!
707734
if (getCodeCatalystDevEnvId() !== undefined) {
708-
const connections = (await this.listConnections()).filter(isBuilderIdConnection)
709-
710-
if (connections.length === 0) {
711-
const key = uuid.v4()
712-
await this.store.addProfile(key, createBuilderIdProfile(codecatalystScopes))
713-
await this.store.setCurrentProfileId(key)
735+
const connections = await this.listConnections()
736+
const shouldInsertDevEnvCredential = !connections.some(isValidCodeCatalystConnection)
737+
738+
if (shouldInsertDevEnvCredential) {
739+
// Insert a profile based on the `~/.aws/config` sso-session:
740+
try {
741+
// After creating a CodeCatalyst Dev Environment profile based on sso-session,
742+
// we discard the actual key, and we insert the profile with an arbitrary key.
743+
// The cache key for any strictly-CodeCatalyst profile is overriden to the
744+
// sso-session identifier on read. If the coerce-on-read semantics ever become
745+
// an issue, it should be possible to use the actual key here However, this
746+
// would require deleting existing profiles to avoid inserting duplicates.
747+
const [_dangerousDiscardActualKey, devEnvProfile] = await this.createCodeCatalystDevEnvProfile()
748+
const key = uuid.v4()
749+
await this.store.addProfile(key, devEnvProfile)
750+
await this.store.setCurrentProfileId(key)
751+
} catch (err) {
752+
getLogger().warn(`auth: failed to insert dev env profile: %s`, err)
753+
}
714754
}
715755
}
716756

src/auth/connection.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ export const isIdcSsoConnection = (conn?: Connection): conn is SsoConnection =>
5252
export const isBuilderIdConnection = (conn?: Connection): conn is SsoConnection => isSsoConnection(conn, 'builderId')
5353

5454
export const isValidCodeCatalystConnection = (conn: Connection): conn is SsoConnection =>
55-
isBuilderIdConnection(conn) && hasScopes(conn, codecatalystScopes)
55+
isSsoConnection(conn) && hasScopes(conn, codecatalystScopes)
5656

5757
export function hasScopes(target: SsoConnection | SsoProfile, scopes: string[]): boolean {
5858
return scopes?.every(s => target.scopes?.includes(s))

src/auth/sso/cache.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -105,25 +105,28 @@ export function getTokenCache(directory = getCacheDir()): KeyedCache<SsoAccess>
105105
}
106106

107107
const logger = (message: string) => getLogger().debug(`SSO token cache: ${message}`)
108-
const cache = createDiskCache<StoredToken, string>((ssoUrl: string) => getTokenCacheFile(directory, ssoUrl), logger)
108+
const cache = createDiskCache<StoredToken, string>((key: string) => getTokenCacheFile(directory, key), logger)
109109

110110
return mapCache(cache, read, write)
111111
}
112112

113-
function getTokenCacheFile(ssoCacheDir: string, ssoUrl: string): string {
114-
const encoded = encodeURI(ssoUrl)
113+
function getTokenCacheFile(ssoCacheDir: string, key: string): string {
114+
const encoded = encodeURI(key)
115115
// Per the spec: 'SSO Login Token Flow' the access token must be
116116
// cached as the SHA1 hash of the bytes of the UTF-8 encoded
117-
// startUrl value with ".json" appended to the end.
117+
// startUrl value with ".json" appended to the end. However, the
118+
// cache key used by the Toolkit is an alternative arbitrary key
119+
// in most scenarios. This alternative cache key still conforms
120+
// to the same ${sha1(key)}.json cache location semantics.
118121

119122
const shasum = crypto.createHash('sha1')
120123
// Suppress warning because:
121124
// 1. SHA1 is prescribed by the AWS SSO spec
122-
// 2. the hashed startUrl value is not a secret
125+
// 2. the hashed startUrl or other key value is not a secret
123126
shasum.update(encoded) // lgtm[js/weak-cryptographic-algorithm]
124-
const hashedUrl = shasum.digest('hex') // lgtm[js/weak-cryptographic-algorithm]
127+
const hashedKey = shasum.digest('hex') // lgtm[js/weak-cryptographic-algorithm]
125128

126-
return path.join(ssoCacheDir, `${hashedUrl}.json`)
129+
return path.join(ssoCacheDir, `${hashedKey}.json`)
127130
}
128131

129132
function getRegistrationCacheFile(ssoCacheDir: string, key: RegistrationKey): string {

src/auth/ui/vue/authForms/manageIdentityCenter.vue

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,45 @@ export class CodeWhispererIdentityCenterState extends BaseIdentityCenterState {
360360
}
361361
}
362362
363+
export class CodeCatalystIdentityCenterState extends BaseIdentityCenterState {
364+
override get id(): AuthFormId {
365+
return 'identityCenterCodeCatalyst'
366+
}
367+
368+
override get name(): string {
369+
return 'CodeCatalyst'
370+
}
371+
372+
override get uiClickOpenId(): AuthUiClick {
373+
return 'auth_openCodeCatalyst'
374+
}
375+
376+
override get uiClickSignout(): AuthUiClick {
377+
return 'auth_codecatalyst_signoutIdentityCenter'
378+
}
379+
380+
override get featureType(): FeatureId {
381+
return 'codecatalyst'
382+
}
383+
384+
protected override async _startIdentityCenterSetup(): Promise<AuthError | undefined> {
385+
const data = await this.getSubmittableDataOrThrow()
386+
return client.startCodeCatalystIdentityCenterSetup(data.startUrl, data.region)
387+
}
388+
389+
override async isAuthConnected(): Promise<boolean> {
390+
return client.isCodeCatalystIdentityCenterConnected()
391+
}
392+
393+
override async showView(): Promise<void> {
394+
client.showCodeCatalystNode()
395+
}
396+
397+
override signout(): Promise<void> {
398+
return client.signoutCodeCatalystIdentityCenter()
399+
}
400+
}
401+
363402
/**
364403
* In the context of the Explorer, an Identity Center connection
365404
* is not required to be active. This is due to us only needing

src/auth/ui/vue/authForms/shared.vue

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
<script lang="ts">
22
import { CodeCatalystBuilderIdState, CodeWhispererBuilderIdState } from './manageBuilderId.vue'
33
import { CredentialsState } from './manageCredentials.vue'
4-
import { CodeWhispererIdentityCenterState, ExplorerIdentityCenterState } from './manageIdentityCenter.vue'
4+
import {
5+
CodeCatalystIdentityCenterState,
6+
CodeWhispererIdentityCenterState,
7+
ExplorerIdentityCenterState,
8+
} from './manageIdentityCenter.vue'
59
import { AuthFormId } from './types'
610
711
/**
@@ -12,6 +16,7 @@ const authFormsState = {
1216
builderIdCodeWhisperer: CodeWhispererBuilderIdState.instance,
1317
builderIdCodeCatalyst: CodeCatalystBuilderIdState.instance,
1418
identityCenterCodeWhisperer: new CodeWhispererIdentityCenterState(),
19+
identityCenterCodeCatalyst: new CodeCatalystIdentityCenterState(),
1520
identityCenterExplorer: new ExplorerIdentityCenterState(),
1621
} as const
1722

src/auth/ui/vue/authForms/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export type AuthFormId =
88
| 'builderIdCodeWhisperer'
99
| 'builderIdCodeCatalyst'
1010
| 'identityCenterCodeWhisperer'
11+
| 'identityCenterCodeCatalyst'
1112
| 'identityCenterExplorer'
1213
| 'aggregateExplorer'
1314

@@ -19,6 +20,7 @@ export const AuthFormDisplayName: Record<AuthFormId, string> = {
1920
credentials: 'IAM Credentials',
2021
builderIdCodeCatalyst: 'CodeCatalyst with AWS Builder ID',
2122
builderIdCodeWhisperer: 'CodeWhisperer with AWS Builder ID',
23+
identityCenterCodeCatalyst: 'CodeCatalyst with IAM Identity Center',
2224
identityCenterCodeWhisperer: 'CodeWhisperer with IAM Identity Center',
2325
identityCenterExplorer: 'AWS Explorer with IAM Identity Center',
2426
aggregateExplorer: '',

src/auth/ui/vue/serviceItemContent/codeCatalystContent.vue

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,34 +32,62 @@
3232
@auth-connection-updated="onAuthConnectionUpdated"
3333
></BuilderIdForm>
3434
</div>
35+
36+
<div>
37+
<div v-on:click="toggleIdentityCenterShown" style="cursor: pointer; display: flex; flex-direction: row">
38+
<div style="font-weight: bold; font-size: medium" :class="identityCenterCollapsibleClass"></div>
39+
<div>
40+
<div style="font-weight: bold; font-size: 14px">Sign in with IAM Identity Center.</div>
41+
</div>
42+
</div>
43+
</div>
44+
45+
<IdentityCenterForm
46+
:state="identityCenterState"
47+
:allow-existing-start-url="true"
48+
@auth-connection-updated="onAuthConnectionUpdated"
49+
v-show="isIdentityCenterShown"
50+
></IdentityCenterForm>
3551
</div>
3652
</div>
3753
</template>
3854

3955
<script lang="ts">
4056
import { defineComponent } from 'vue'
4157
import BuilderIdForm, { CodeCatalystBuilderIdState } from '../authForms/manageBuilderId.vue'
58+
import IdentityCenterForm, { CodeCatalystIdentityCenterState } from '../authForms/manageIdentityCenter.vue'
4259
import BaseServiceItemContent from './baseServiceItemContent.vue'
4360
import authFormsState, { AuthForm, FeatureStatus } from '../authForms/shared.vue'
4461
import { AuthFormId } from '../authForms/types'
4562
import { ConnectionUpdateArgs } from '../authForms/baseAuth.vue'
63+
import { WebviewClientFactory } from '../../../../webviews/client'
64+
import { AuthWebview } from '../show'
65+
66+
const client = WebviewClientFactory.create<AuthWebview>()
4667
4768
export default defineComponent({
4869
name: 'CodeCatalystContent',
49-
components: { BuilderIdForm },
70+
components: { BuilderIdForm, IdentityCenterForm },
5071
extends: BaseServiceItemContent,
5172
data() {
5273
return {
5374
isLoaded: {
5475
builderIdCodeCatalyst: false,
5576
} as Record<AuthFormId, boolean>,
5677
isAllAuthsLoaded: false,
78+
isIdentityCenterShown: false,
5779
}
5880
},
5981
computed: {
6082
builderIdState(): CodeCatalystBuilderIdState {
6183
return authFormsState.builderIdCodeCatalyst
6284
},
85+
identityCenterState(): CodeCatalystIdentityCenterState {
86+
return authFormsState.identityCenterCodeCatalyst
87+
},
88+
identityCenterCollapsibleClass() {
89+
return this.isIdentityCenterShown ? 'icon icon-vscode-chevron-down' : 'icon icon-vscode-chevron-right'
90+
},
6391
},
6492
methods: {
6593
updateIsAllAuthsLoaded() {
@@ -71,12 +99,18 @@ export default defineComponent({
7199
this.updateIsAllAuthsLoaded()
72100
this.emitAuthConnectionUpdated('codecatalyst', args)
73101
},
102+
toggleIdentityCenterShown() {
103+
this.isIdentityCenterShown = !this.isIdentityCenterShown
104+
if (this.isIdentityCenterShown) {
105+
client.emitUiClick('auth_codecatalyst_expandIAMIdentityCenter')
106+
}
107+
},
74108
},
75109
})
76110
77111
export class CodeCatalystContentState extends FeatureStatus {
78112
override getAuthForms(): AuthForm[] {
79-
return [authFormsState.builderIdCodeCatalyst]
113+
return [authFormsState.builderIdCodeCatalyst, authFormsState.identityCenterCodeCatalyst]
80114
}
81115
}
82116
</script>

0 commit comments

Comments
 (0)