Skip to content

Commit b91972f

Browse files
auth: Helper methods to check if connection exists (#3747)
* auth: funcs to indicate if the connection exists Signed-off-by: Nikolas Komonen <[email protected]> * codewhisperer: method that indicates if valid connection exists For the add connection telemetry we want to know if a CW auth' connection already exists, not necessarily the currently active one. This adds 2 methods that return true if the valid CW connection exists and is known by the toolkit. Signed-off-by: Nikolas Komonen <[email protected]> * refactor: create isSsoConnection() alternatives A sso connection contains different types of sso connections, such as Builder ID or AWS Identity Center (Base). This commit creates functions to enable a user to check with specificity of if a connection is of a specific type of sso connection. Additionally, it updates existing uses to the appropriate implementation. Signed-off-by: Nikolas Komonen <[email protected]> * codecatalyst: function if toolkit is aware of valid builder id Signed-off-by: Nikolas Komonen <[email protected]> * refactor: fix circular dep issue Moves isValidCodeCatalystConnection() to the auth/connections.ts module and it fixes the circular dep Signed-off-by: Nikolas Komonen <[email protected]> * refactor: rename 'base' sso to 'idc' sso This naming is easier to understand even though the term Identity Center (idc) is purely a brand name and subject to change in the future. Signed-off-by: nkomonen <[email protected]> --------- Signed-off-by: Nikolas Komonen <[email protected]> Signed-off-by: nkomonen <[email protected]>
1 parent 65a06f4 commit b91972f

File tree

9 files changed

+245
-20
lines changed

9 files changed

+245
-20
lines changed

src/auth/connection.ts

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,36 @@ export const ssoAccountAccessScopes = ['sso:account:access']
2323
export const codewhispererScopes = ['codewhisperer:completions', 'codewhisperer:analysis']
2424
export const defaultSsoRegion = 'us-east-1'
2525

26+
type SsoType =
27+
| 'any' // any type of sso
28+
| 'idc' // AWS Identity Center
29+
| 'builderId'
30+
2631
export const isIamConnection = (conn?: Connection): conn is IamConnection => conn?.type === 'iam'
27-
export const isSsoConnection = (conn?: Connection): conn is SsoConnection => conn?.type === 'sso'
28-
export const isBuilderIdConnection = (conn?: Connection): conn is SsoConnection =>
29-
isSsoConnection(conn) && conn.startUrl === builderIdStartUrl
32+
export const isSsoConnection = (conn?: Connection, type: SsoType = 'any'): conn is SsoConnection => {
33+
if (conn?.type !== 'sso') {
34+
return false
35+
}
36+
// At this point the conn is an SSO conn, but now we must determine the specific type
37+
switch (type) {
38+
case 'idc':
39+
// An Identity Center SSO connection is the Base/Root and doesn't
40+
// have any unique identifiers, so we must eliminate the other SSO
41+
// types to determine if this is Identity Center.
42+
// This condition should grow as more SsoType's get added.
43+
return !isBuilderIdConnection(conn)
44+
case 'builderId':
45+
return conn.startUrl === builderIdStartUrl
46+
case 'any':
47+
return true
48+
}
49+
}
50+
export const isAnySsoConnection = (conn?: Connection): conn is SsoConnection => isSsoConnection(conn, 'any')
51+
export const isIdcSsoConnection = (conn?: Connection): conn is SsoConnection => isSsoConnection(conn, 'idc')
52+
export const isBuilderIdConnection = (conn?: Connection): conn is SsoConnection => isSsoConnection(conn, 'builderId')
53+
54+
export const isValidCodeCatalystConnection = (conn: Connection): conn is SsoConnection =>
55+
isBuilderIdConnection(conn) && hasScopes(conn, codecatalystScopes)
3056

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

src/auth/sso/validation.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import * as vscode from 'vscode'
66
import { UnknownError } from '../../shared/errors'
77
import { AuthType } from '../auth'
8-
import { isSsoConnection, SsoConnection, hasScopes } from '../connection'
8+
import { SsoConnection, hasScopes, isAnySsoConnection } from '../connection'
99

1010
export function validateSsoUrl(auth: AuthType, url: string, requiredScopes?: string[]) {
1111
const urlFormatError = validateSsoUrlFormat(url)
@@ -28,7 +28,7 @@ export async function validateIsNewSsoUrlAsync(
2828
requiredScopes?: string[]
2929
): Promise<string | undefined> {
3030
return auth.listConnections().then(conns => {
31-
return validateIsNewSsoUrl(url, requiredScopes, conns.filter(isSsoConnection))
31+
return validateIsNewSsoUrl(url, requiredScopes, conns.filter(isAnySsoConnection))
3232
})
3333
}
3434

src/auth/utils.ts

Lines changed: 82 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,18 +32,23 @@ import { SectionName, StaticProfile } from './credentials/types'
3232
import { throwOnInvalidCredentials } from './credentials/validation'
3333
import {
3434
Connection,
35+
SsoConnection,
3536
createBuilderIdProfile,
3637
createSsoProfile,
3738
defaultSsoRegion,
39+
isAnySsoConnection,
40+
isIdcSsoConnection,
3841
isBuilderIdConnection,
39-
isSsoConnection,
42+
isIamConnection,
43+
isValidCodeCatalystConnection,
4044
} from './connection'
4145
import { Commands } from '../shared/vscode/commands2'
4246
import { Auth } from './auth'
4347
import { validateIsNewSsoUrl, validateSsoUrlFormat } from './sso/validation'
4448
import { openUrl } from '../shared/utilities/vsCodeUtils'
4549
import { AuthSource } from './ui/vue/show'
4650
import { getLogger } from '../shared/logger'
51+
import { isValidCodeWhispererConnection } from '../codewhisperer/util/authUtil'
4752

4853
// TODO: Look to do some refactoring to handle circular dependency later and move this to ./commands.ts
4954
export const showConnectionsPageCommand = 'aws.auth.manageConnections'
@@ -144,7 +149,7 @@ export const createIamItem = () =>
144149
} as DataQuickPickItem<'iam'>)
145150

146151
export async function createStartUrlPrompter(title: string, requiredScopes?: string[]) {
147-
const existingConnections = (await Auth.instance.listConnections()).filter(isSsoConnection)
152+
const existingConnections = (await Auth.instance.listConnections()).filter(isAnySsoConnection)
148153

149154
function validateSsoUrl(url: string) {
150155
const urlFormatError = validateSsoUrlFormat(url)
@@ -558,6 +563,81 @@ export class AuthNode implements TreeNode<Auth> {
558563
}
559564
}
560565

566+
export async function hasIamCredentials(
567+
allConnections = () => Auth.instance.listAndTraverseConnections().promise()
568+
): Promise<boolean> {
569+
return (await allConnections()).some(isIamConnection)
570+
}
571+
572+
export type SsoKind = 'any' | 'codewhisperer'
573+
574+
/**
575+
* Returns true if an Identity Center SSO connection exists.
576+
*
577+
* @param kind A specific kind of Identity Center SSO connection that must exist.
578+
* @param allConnections func to get all connections that exist
579+
*/
580+
export async function hasSso(
581+
kind: SsoKind = 'any',
582+
allConnections = () => Auth.instance.listConnections()
583+
): Promise<boolean> {
584+
return (await findSsoConnections(kind, allConnections)).length > 0
585+
}
586+
587+
async function findSsoConnections(
588+
kind: SsoKind = 'any',
589+
allConnections = () => Auth.instance.listConnections()
590+
): Promise<SsoConnection[]> {
591+
let predicate: (c?: Connection) => boolean
592+
switch (kind) {
593+
case 'codewhisperer':
594+
predicate = (conn?: Connection) => {
595+
return isIdcSsoConnection(conn) && isValidCodeWhispererConnection(conn)
596+
}
597+
break
598+
case 'any':
599+
predicate = isIdcSsoConnection
600+
}
601+
return (await allConnections()).filter(predicate).filter(isIdcSsoConnection)
602+
}
603+
604+
export type BuilderIdKind = 'any' | 'codewhisperer' | 'codecatalyst'
605+
606+
/**
607+
* Returns true if a Builder ID connection exists.
608+
*
609+
* @param kind A Builder ID connection that has the scopes of this kind.
610+
* @param allConnections func to get all connections that exist
611+
*/
612+
export async function hasBuilderId(
613+
kind: BuilderIdKind = 'any',
614+
allConnections = () => Auth.instance.listConnections()
615+
): Promise<boolean> {
616+
return (await findBuilderIdConnections(kind, allConnections)).length > 0
617+
}
618+
619+
async function findBuilderIdConnections(
620+
kind: BuilderIdKind = 'any',
621+
allConnections = () => Auth.instance.listConnections()
622+
): Promise<SsoConnection[]> {
623+
let predicate: (c?: Connection) => boolean
624+
switch (kind) {
625+
case 'codewhisperer':
626+
predicate = (conn?: Connection) => {
627+
return isBuilderIdConnection(conn) && isValidCodeWhispererConnection(conn)
628+
}
629+
break
630+
case 'codecatalyst':
631+
predicate = (conn?: Connection) => {
632+
return isBuilderIdConnection(conn) && isValidCodeCatalystConnection(conn)
633+
}
634+
break
635+
case 'any':
636+
predicate = isBuilderIdConnection
637+
}
638+
return (await allConnections()).filter(predicate).filter(isAnySsoConnection)
639+
}
640+
561641
/**
562642
* Class to get info about the user + use of this extension
563643
*

src/codecatalyst/auth.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@ import {
1515
ssoAccountAccessScopes,
1616
codecatalystScopes,
1717
SsoConnection,
18-
hasScopes,
1918
Connection,
2019
isBuilderIdConnection,
20+
isValidCodeCatalystConnection,
2121
} from '../auth/connection'
2222
import { createBuilderIdConnection } from '../auth/utils'
2323

@@ -39,8 +39,6 @@ export class CodeCatalystAuthStorage {
3939
export const onboardingUrl = vscode.Uri.parse('https://codecatalyst.aws/onboarding/view')
4040

4141
const defaultScopes = [...ssoAccountAccessScopes, ...codecatalystScopes]
42-
export const isValidCodeCatalystConnection = (conn: Connection): conn is SsoConnection =>
43-
isBuilderIdConnection(conn) && hasScopes(conn, codecatalystScopes)
4442

4543
export const isUpgradeableConnection = (conn: Connection): conn is SsoConnection =>
4644
isBuilderIdConnection(conn) && !isValidCodeCatalystConnection(conn)

src/codewhisperer/util/authUtil.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import { getLogger } from '../../shared/logger'
2727
export const defaultCwScopes = [...ssoAccountAccessScopes, ...codewhispererScopes]
2828
export const awsBuilderIdSsoProfile = createBuilderIdProfile(defaultCwScopes)
2929

30-
export const isValidCodeWhispererConnection = (conn: Connection): conn is Connection => {
30+
export const isValidCodeWhispererConnection = (conn?: Connection): conn is Connection => {
3131
if (isCloud9('classic')) {
3232
return isIamConnection(conn)
3333
}

src/test/codewhisperer/util/authUtil.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { getTestWindow } from '../../shared/vscode/window'
99
import { SeverityLevel } from '../../shared/vscode/message'
1010
import { createBuilderIdProfile, createSsoProfile, createTestAuth } from '../../credentials/testUtil'
1111
import { captureEventOnce } from '../../testUtil'
12-
import { codewhispererScopes, isSsoConnection } from '../../../auth/connection'
12+
import { codewhispererScopes, isAnySsoConnection, isBuilderIdConnection } from '../../../auth/connection'
1313

1414
const enterpriseSsoStartUrl = 'https://enterprise.awsapps.com/start'
1515

@@ -111,9 +111,9 @@ describe('AuthUtil', async function () {
111111
await authUtil.connectToAwsBuilderId()
112112
assert.ok(authUtil.isConnected())
113113

114-
const ssoConnectionIds = new Set(auth.activeConnectionEvents.emits.filter(isSsoConnection).map(c => c.id))
114+
const ssoConnectionIds = new Set(auth.activeConnectionEvents.emits.filter(isAnySsoConnection).map(c => c.id))
115115
assert.strictEqual(ssoConnectionIds.size, 1, 'Expected exactly 1 unique SSO connection id')
116-
assert.strictEqual((await auth.listConnections()).filter(isSsoConnection).length, 1)
116+
assert.strictEqual((await auth.listConnections()).filter(isAnySsoConnection).length, 1)
117117
})
118118

119119
it('automatically upgrades connections if they do not have the required scopes', async function () {
@@ -124,11 +124,11 @@ describe('AuthUtil', async function () {
124124
await authUtil.connectToAwsBuilderId()
125125
assert.ok(authUtil.isConnected())
126126
assert.ok(authUtil.isConnectionValid())
127-
assert.ok(isSsoConnection(authUtil.conn))
127+
assert.ok(isBuilderIdConnection(authUtil.conn))
128128
assert.strictEqual(authUtil.conn?.id, upgradeableConn.id)
129129
assert.strictEqual(authUtil.conn.startUrl, upgradeableConn.startUrl)
130130
assert.strictEqual(authUtil.conn.ssoRegion, upgradeableConn.ssoRegion)
131-
assert.strictEqual((await auth.listConnections()).filter(isSsoConnection).length, 1)
131+
assert.strictEqual((await auth.listConnections()).filter(isAnySsoConnection).length, 1)
132132
})
133133

134134
it('test reformatStartUrl should remove trailing slash and hash', function () {

src/test/credentials/testUtil.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,20 @@ import globals from '../../shared/extensionGlobals'
1717
import { fromString } from '../../auth/providers/credentials'
1818
import { mergeAndValidateSections, parseIni } from '../../auth/credentials/sharedCredentials'
1919
import { SharedCredentialsProvider } from '../../auth/providers/sharedCredentialsProvider'
20-
import { Connection, ProfileStore, SsoConnection, SsoProfile } from '../../auth/connection'
20+
import { Connection, IamConnection, ProfileStore, SsoConnection, SsoProfile } from '../../auth/connection'
21+
import * as sinon from 'sinon'
22+
23+
/** Mock Connection objects for test usage */
24+
export const ssoConnection: SsoConnection = {
25+
type: 'sso',
26+
id: '0',
27+
label: 'sso',
28+
ssoRegion: 'us-east-1',
29+
startUrl: 'https://nkomonen.awsapps.com/start',
30+
getToken: sinon.stub(),
31+
}
32+
export const builderIdConnection: SsoConnection = { ...ssoConnection, startUrl: builderIdStartUrl, label: 'builderId' }
33+
export const iamConnection: IamConnection = { type: 'iam', id: '0', label: 'iam', getCredentials: sinon.stub() }
2134

2235
export function createSsoProfile(props?: Partial<Omit<SsoProfile, 'type'>>): SsoProfile {
2336
return {

src/test/credentials/utils.test.ts

Lines changed: 104 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
import * as vscode from 'vscode'
66
import assert from 'assert'
77
import { FakeExtensionContext } from '../fakeExtensionContext'
8-
import { ExtensionUse } from '../../auth/utils'
8+
import { BuilderIdKind, ExtensionUse, SsoKind, hasBuilderId, hasIamCredentials, hasSso } from '../../auth/utils'
9+
import { Connection, SsoConnection, codecatalystScopes, codewhispererScopes } from '../../auth/connection'
10+
import { builderIdConnection, iamConnection, ssoConnection } from './testUtil'
911

1012
describe('ExtensionUse.isFirstUse()', function () {
1113
let fakeState: vscode.Memento
@@ -62,3 +64,104 @@ describe('ExtensionUse.isFirstUse()', function () {
6264
return new ExtensionUse()
6365
}
6466
})
67+
68+
type SsoTestCase = { kind: SsoKind; connections: Connection[]; expected: boolean }
69+
type BuilderIdTestCase = { kind: BuilderIdKind; connections: Connection[]; expected: boolean }
70+
71+
describe('connection exists funcs', function () {
72+
const cwIdcConnection: SsoConnection = { ...ssoConnection, scopes: codewhispererScopes, label: 'codeWhispererSso' }
73+
const cwBuilderIdConnection: SsoConnection = {
74+
...builderIdConnection,
75+
scopes: codewhispererScopes,
76+
label: 'codeWhispererBuilderId',
77+
}
78+
const ccBuilderIdConnection: SsoConnection = {
79+
...builderIdConnection,
80+
scopes: codecatalystScopes,
81+
label: 'codeCatalystBuilderId',
82+
}
83+
const ssoConnections: Connection[] = [
84+
ssoConnection,
85+
builderIdConnection,
86+
cwIdcConnection,
87+
cwBuilderIdConnection,
88+
ccBuilderIdConnection,
89+
]
90+
const allConnections = [iamConnection, ...ssoConnections]
91+
92+
describe('ssoExists()', function () {
93+
const anyCases: SsoTestCase[] = [
94+
{ connections: [ssoConnection], expected: true },
95+
{ connections: allConnections, expected: true },
96+
{ connections: [], expected: false },
97+
{ connections: [iamConnection], expected: false },
98+
].map(c => {
99+
return { ...c, kind: 'any' }
100+
})
101+
const cwIdcCases: SsoTestCase[] = [
102+
{ connections: [cwIdcConnection], expected: true },
103+
{ connections: allConnections, expected: true },
104+
{ connections: [], expected: false },
105+
{ connections: allConnections.filter(c => c !== cwIdcConnection), expected: false },
106+
].map(c => {
107+
return { ...c, kind: 'codewhisperer' }
108+
})
109+
const allCases = [...anyCases, ...cwIdcCases]
110+
111+
allCases.forEach(args => {
112+
it(`ssoExists() returns '${args.expected}' when kind '${args.kind}' given [${args.connections
113+
.map(c => c.label)
114+
.join(', ')}]`, async function () {
115+
assert.strictEqual(await hasSso(args.kind, async () => args.connections), args.expected)
116+
})
117+
})
118+
})
119+
120+
describe('builderIdExists()', function () {
121+
const cwBuilderIdCases: BuilderIdTestCase[] = [
122+
{ connections: [cwBuilderIdConnection], expected: true },
123+
{ connections: allConnections, expected: true },
124+
{ connections: [], expected: false },
125+
{ connections: allConnections.filter(c => c !== cwBuilderIdConnection), expected: false },
126+
].map(c => {
127+
return { ...c, kind: 'codewhisperer' }
128+
})
129+
130+
const ccBuilderIdCases: BuilderIdTestCase[] = [
131+
{connections: [ccBuilderIdConnection], expected: true},
132+
{connections: allConnections, expected: true},
133+
{connections: [], expected: false},
134+
{connections: allConnections.filter(c => c !== ccBuilderIdConnection), expected: false},
135+
].map(c => { return {...c, kind: 'codecatalyst'}})
136+
137+
const allCases = [...cwBuilderIdCases, ...ccBuilderIdCases]
138+
139+
allCases.forEach(args => {
140+
it(`builderIdExists() returns '${args.expected}' when kind '${args.kind}' given [${args.connections
141+
.map(c => c.label)
142+
.join(', ')}]`, async function () {
143+
assert.strictEqual(await hasBuilderId(args.kind, async () => args.connections), args.expected)
144+
})
145+
})
146+
})
147+
148+
describe('credentialExists()', function () {
149+
const cases: [Connection[], boolean][] = [
150+
[[iamConnection], true],
151+
[allConnections, true],
152+
[[], false],
153+
[allConnections.filter(c => c !== iamConnection), false],
154+
]
155+
156+
cases.forEach(args => {
157+
it(`credentialExists() returns '${args[1]}' given [${args[0]
158+
.map(c => c.label)
159+
.join(', ')}]`, async function () {
160+
const connections = args[0]
161+
const expected = args[1]
162+
163+
assert.strictEqual(await hasIamCredentials(async () => connections), expected)
164+
})
165+
})
166+
})
167+
})

src/testE2E/codecatalyst/client.test.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
} from '../../shared/clients/codecatalystClient'
1515
import { getThisDevEnv, prepareDevEnvConnection } from '../../codecatalyst/model'
1616
import { Auth } from '../../auth/auth'
17-
import { CodeCatalystAuthenticationProvider, isValidCodeCatalystConnection } from '../../codecatalyst/auth'
17+
import { CodeCatalystAuthenticationProvider } from '../../codecatalyst/auth'
1818
import { CodeCatalystCommands, DevEnvironmentSettings } from '../../codecatalyst/commands'
1919
import globals from '../../shared/extensionGlobals'
2020
import { CodeCatalystCreateWebview, SourceResponse } from '../../codecatalyst/vue/create/backend'
@@ -30,7 +30,12 @@ import { toStream } from '../../shared/utilities/collectionUtils'
3030
import { toCollection } from '../../shared/utilities/asyncCollection'
3131
import { getLogger } from '../../shared/logger'
3232
import { isAwsError } from '../../shared/errors'
33-
import { codecatalystScopes, createBuilderIdProfile, SsoConnection } from '../../auth/connection'
33+
import {
34+
codecatalystScopes,
35+
createBuilderIdProfile,
36+
isValidCodeCatalystConnection,
37+
SsoConnection,
38+
} from '../../auth/connection'
3439

3540
let spaceName: CodeCatalystOrg['name']
3641
let projectName: CodeCatalystProject['name']

0 commit comments

Comments
 (0)