Skip to content

Commit e763827

Browse files
authWebview: Identity Center sign in for Explorer (#3597)
* identityCenter: remove delayed rendering mechanism Previously this component had a mechanism to not show its inner content until it was all done loading. But from a recent change this is not needed anymore since it is done in the parent component instead. Solution: Remove the old mechanism Signed-off-by: Nikolas Komonen <[email protected]> * identityCenter: Update some backend methods The existing identity center methods were codewhisperer centric. This commit updates and adds method for general uses of identity center as well. Signed-off-by: Nikolas Komonen <[email protected]> * identityCenter: Add identity center form state This adds the class which manages the state of identity center in the context of the Explorer. In the context of the Explorer, Identity Center form behaviour is slightly different which is reflected in some of the implementations of the class. In short, IC in Explorer is not required to be actively connected. Instead it is just used to get the Credentials profile information. So we do not care about the active IC connection in this scenario Signed-off-by: Nikolas Komonen <[email protected]> * identityCenter: Fix 'waiting for user' stage not being shown The waiting for user stage is shown once the user starts sso signup process. It wasn't activated in the actual UI, this fixes that Signed-off-by: Nikolas Komonen <[email protected]> * identityCenter: Show different form title for Explorer With Identity Center in the context of the Explorer we do not care about the actual 'active' status of an Identity Center connection. Due to this we don't want to show anything regarding a 'successful' connection in the form title. Solution: Have a conditional title that simply shows the name if we set a certain Vue Prop and ignores the connected stuff. Signed-off-by: Nikolas Komonen <[email protected]> * identityCenter: partial explorer form setup I still need to add the extra form that will aggregate the IC and Credentials status in to one. Signed-off-by: Nikolas Komonen <[email protected]> * identityCenter: somewhat finished explorer window Still reloads the whole webview upon submission when I have an existing successful connection. Due to this if there is an error I see it briefly before the whole page is reloaded. Signed-off-by: Nikolas Komonen <[email protected]> * identityCenter: option for credentials to always show form In the context of the resource explorer auth sign in we want to allow users to add additional IAM credentials even if they already have an existing one. Solution: In the Credentials form add a mechanism to specify that we do not care about checking if credentials is currently connected. This will allow users to continuously add new credentials to whoever sets the prop checkIfConnected when creating the component. Additional: Fixed identity center not updating since it used the same mechanism but didn't emit the proper isConnected status in the background. We still want to emit the true isConnected status, but in the case of checkIfConnected being false we don't want to show the user that state. Signed-off-by: Nikolas Komonen <[email protected]> * identityCenter: show correct connection name once connected The ExplorerAggregateForm that represents the status of all connections relevant to the Explorer will now show the name of the specific underyling connection that enables it to be connected. Signed-off-by: Nikolas Komonen <[email protected]> * identityCenter: Force loading of SSO profiles When we add the new Identity Center/SSO connection we will now trigger loading of connected Credentials profiles in to the profile store. We are doing this so that users of this information are updated with the new profiles. Signed-off-by: Nikolas Komonen <[email protected]> --------- Signed-off-by: Nikolas Komonen <[email protected]>
1 parent ac50961 commit e763827

File tree

8 files changed

+361
-78
lines changed

8 files changed

+361
-78
lines changed

src/auth/ui/vue/authForms/manageCredentials.vue

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
<div>
44
<FormTitle :isConnected="isConnected">IAM Credentials</FormTitle>
55

6-
<div class="form-section">
7-
<button v-if="isConnected" v-on:click="showResourceExplorer">Open Resource Explorer</button>
6+
<div class="form-section" v-if="isConnected">
7+
<button v-on:click="showResourceExplorer">Open Resource Explorer</button>
88
</div>
99

1010
<div v-if="isConnected" class="form-section" v-on:click="toggleShowForm()" id="collapsible">
@@ -76,6 +76,13 @@ export default defineComponent({
7676
type: Object as PropType<CredentialsState>,
7777
required: true,
7878
},
79+
checkIfConnected: {
80+
type: Boolean,
81+
default: true,
82+
// In some scenarios we want to show the form and allow setup,
83+
// but not care about any current identity center auth connections
84+
// and if they are connected or not.
85+
},
7986
},
8087
data() {
8188
return {
@@ -104,7 +111,7 @@ export default defineComponent({
104111
await this.updateDataError('profileName')
105112
await this.updateDataError('aws_access_key_id')
106113
await this.updateDataError('aws_secret_access_key')
107-
this.isFormShown = !(await this.state.isAuthConnected())
114+
this.isFormShown = this.checkIfConnected ? !(await this.state.isAuthConnected()) : true
108115
await this.updateSubmittableStatus()
109116
110117
this.updateConnectedStatus('created')
@@ -138,8 +145,8 @@ export default defineComponent({
138145
},
139146
async updateConnectedStatus(cause?: ConnectionUpdateCause) {
140147
return this.state.isAuthConnected().then(isConnected => {
141-
this.isConnected = isConnected
142-
this.emitAuthConnectionUpdated({ id: 'credentials', isConnected: this.isConnected, cause })
148+
this.isConnected = this.checkIfConnected ? isConnected : false
149+
this.emitAuthConnectionUpdated({ id: 'credentials', isConnected, cause })
143150
})
144151
},
145152
async submitData() {
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
<template>
2+
<div class="auth-form container-background border-common" id="identity-center-form">
3+
<div>
4+
<FormTitle :isConnected="isConnected">{{ connectionName }}</FormTitle>
5+
<div v-if="!isConnected">Successor to AWS Single Sign-on</div>
6+
</div>
7+
8+
<div v-if="isConnected" class="form-section">
9+
<button v-on:click="showExplorer()">Open Resource Explorer</button>
10+
</div>
11+
</div>
12+
</template>
13+
<script lang="ts">
14+
import { PropType, defineComponent } from 'vue'
15+
import BaseAuthForm from './baseAuth.vue'
16+
import FormTitle from './formTitle.vue'
17+
import { WebviewClientFactory } from '../../../../webviews/client'
18+
import { AuthWebview } from '../show'
19+
import { ExplorerIdentityCenterState } from './manageIdentityCenter.vue'
20+
import { CredentialsState } from './manageCredentials.vue'
21+
import { AuthFormId } from './types'
22+
23+
const client = WebviewClientFactory.create<AuthWebview>()
24+
25+
export type IdentityCenterStage = 'START' | 'WAITING_ON_USER' | 'CONNECTED'
26+
27+
/**
28+
* This component is used to represent all of the multiple auth
29+
* mechanisms in one place. It aggregates the possible auth mechanisms
30+
* and if one of them are connected this will show that the explorer
31+
* is successfully connected.
32+
*/
33+
export default defineComponent({
34+
name: 'ExplorerAggregateForm',
35+
extends: BaseAuthForm,
36+
components: { FormTitle },
37+
props: {
38+
identityCenterState: {
39+
type: Object as PropType<ExplorerIdentityCenterState>,
40+
required: true,
41+
},
42+
credentialsState: {
43+
type: Object as PropType<CredentialsState>,
44+
required: true,
45+
},
46+
},
47+
data() {
48+
return {
49+
isConnected: false,
50+
connectionName: '',
51+
}
52+
},
53+
54+
async created() {
55+
this.isConnected =
56+
(await this.credentialsState.isAuthConnected()) || (await this.identityCenterState.isAuthConnected())
57+
await this.updateConnectionName()
58+
this.emitAuthConnectionUpdated({ id: 'aggregateExplorer', isConnected: this.isConnected, cause: 'created' })
59+
},
60+
methods: {
61+
showExplorer() {
62+
client.showResourceExplorer()
63+
},
64+
async updateConnectionName() {
65+
const currentConnection = await this.getCurrentConnection()
66+
if (currentConnection === undefined) {
67+
this.connectionName = ''
68+
} else {
69+
this.connectionName = currentConnection === 'credentials' ? 'IAM Credentials' : 'IAM Identity Center'
70+
}
71+
},
72+
/**
73+
* Gets the current working connection that the explorer can use.
74+
*/
75+
async getCurrentConnection(): Promise<AuthFormId | undefined> {
76+
if (!this.isConnected) {
77+
return undefined
78+
}
79+
return (await this.credentialsState.isAuthConnected()) ? 'credentials' : 'identityCenterExplorer'
80+
},
81+
},
82+
})
83+
</script>
84+
<style>
85+
@import './sharedAuthForms.css';
86+
@import '../shared.css';
87+
88+
#identity-center-form {
89+
width: 280px;
90+
height: fit-content;
91+
}
92+
</style>

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

Lines changed: 101 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,56 @@
11
<template>
22
<div class="auth-form container-background border-common" id="identity-center-form">
3-
<div v-show="canShowAll">
3+
<div v-if="checkIfConnected">
44
<FormTitle :isConnected="isConnected">IAM Identity Center</FormTitle>
55
<div v-if="!isConnected">Successor to AWS Single Sign-on</div>
6+
</div>
7+
<div v-else>
8+
<!-- In this scenario we do not care about the active IC connection -->
9+
<FormTitle :isConnected="false">IAM Identity Center</FormTitle>
10+
<div>Successor to AWS Single Sign-on</div>
11+
</div>
12+
13+
<div v-if="stage === 'START'">
14+
<div class="form-section">
15+
<a href="https://docs.aws.amazon.com/toolkit-for-vscode/latest/userguide/sso-credentials.html"
16+
>Learn more about IAM Identity Center.</a
17+
>
18+
</div>
19+
20+
<div class="form-section">
21+
<label class="input-title">Start URL</label>
22+
<label class="small-description">The Start URL</label>
23+
<input v-model="data.startUrl" type="text" :data-invalid="!!errors.startUrl" />
24+
<div class="small-description error-text">{{ errors.startUrl }}</div>
25+
</div>
26+
27+
<div class="form-section">
28+
<label class="input-title">Region</label>
29+
<label class="small-description">The Region</label>
630

7-
<div v-if="stage === 'START'">
8-
<div class="form-section">
9-
<a href="https://docs.aws.amazon.com/toolkit-for-vscode/latest/userguide/sso-credentials.html"
10-
>Learn more about IAM Identity Center.</a
11-
>
12-
</div>
13-
14-
<div class="form-section">
15-
<label class="input-title">Start URL</label>
16-
<label class="small-description">The Start URL</label>
17-
<input v-model="data.startUrl" type="text" :data-invalid="!!errors.startUrl" />
18-
<div class="small-description error-text">{{ errors.startUrl }}</div>
19-
</div>
20-
21-
<div class="form-section">
22-
<label class="input-title">Region</label>
23-
<label class="small-description">The Region</label>
24-
25-
<select v-on:click="getRegion()">
26-
<option v-if="!!data.region" :selected="true">{{ data.region }}</option>
27-
</select>
28-
</div>
29-
30-
<div class="form-section">
31-
<button v-on:click="signin()" :disabled="!canSubmit">Sign up or Sign in</button>
32-
</div>
31+
<select v-on:click="getRegion()">
32+
<option v-if="!!data.region" :selected="true">{{ data.region }}</option>
33+
</select>
3334
</div>
3435

35-
<div v-if="stage === 'WAITING_ON_USER'">
36-
<div class="form-section">
37-
<div>Follow instructions...</div>
38-
</div>
36+
<div class="form-section">
37+
<button v-on:click="signin()" :disabled="!canSubmit">Sign up or Sign in</button>
38+
</div>
39+
</div>
40+
41+
<div v-if="stage === 'WAITING_ON_USER'">
42+
<div class="form-section">
43+
<div>Follow instructions...</div>
3944
</div>
45+
</div>
4046

41-
<div v-if="stage === 'CONNECTED'">
42-
<div class="form-section">
43-
<div v-on:click="signout()" style="cursor: pointer; color: #75beff">Sign out</div>
44-
</div>
47+
<div v-if="stage === 'CONNECTED'">
48+
<div class="form-section">
49+
<div v-on:click="signout()" style="cursor: pointer; color: #75beff">Sign out</div>
50+
</div>
4551

46-
<div class="form-section">
47-
<button v-on:click="showView()">Open {{ authName }} in Toolkit</button>
48-
</div>
52+
<div class="form-section">
53+
<button v-on:click="showView()">Open {{ authName }} in Toolkit</button>
4954
</div>
5055
</div>
5156
</div>
@@ -73,6 +78,13 @@ export default defineComponent({
7378
type: Object as PropType<BaseIdentityCenterState>,
7479
required: true,
7580
},
81+
checkIfConnected: {
82+
type: Boolean,
83+
default: true,
84+
// In some scenarios we want to show the form and allow setup,
85+
// but not care about any current identity center auth connections
86+
// and if they are connected or not.
87+
},
7688
},
7789
data() {
7890
return {
@@ -88,7 +100,6 @@ export default defineComponent({
88100
89101
stage: 'START' as IdentityCenterStage,
90102
91-
canShowAll: false,
92103
authName: this.state.name,
93104
}
94105
},
@@ -99,11 +110,11 @@ export default defineComponent({
99110
this.data.region = this.state.getValue('region')
100111
101112
await this.update('created')
102-
this.canShowAll = true
103113
},
104114
computed: {},
105115
methods: {
106116
async signin(): Promise<void> {
117+
this.stage = 'WAITING_ON_USER'
107118
await this.state.startIdentityCenterSetup()
108119
await this.update('signIn')
109120
},
@@ -113,8 +124,9 @@ export default defineComponent({
113124
},
114125
async update(cause?: ConnectionUpdateCause) {
115126
this.stage = await this.state.stage()
116-
this.isConnected = await this.state.isAuthConnected()
117-
this.emitAuthConnectionUpdated({ id: this.state.id, isConnected: this.isConnected, cause })
127+
const actualIsConnected = await this.state.isAuthConnected()
128+
this.isConnected = this.checkIfConnected ? actualIsConnected : false
129+
this.emitAuthConnectionUpdated({ id: this.state.id, isConnected: actualIsConnected, cause })
118130
},
119131
async getRegion() {
120132
const region = await this.state.getRegion()
@@ -165,6 +177,7 @@ abstract class BaseIdentityCenterState implements AuthStatus {
165177
protected abstract _startIdentityCenterSetup(): Promise<void>
166178
abstract isAuthConnected(): Promise<boolean>
167179
abstract showView(): Promise<void>
180+
abstract signout(): Promise<void>
168181
169182
setValue(key: IdentityCenterKey, value: string) {
170183
this._data[key] = value
@@ -185,10 +198,6 @@ abstract class BaseIdentityCenterState implements AuthStatus {
185198
return this._stage
186199
}
187200
188-
async signout(): Promise<void> {
189-
return client.signoutIdentityCenter()
190-
}
191-
192201
async getRegion(): Promise<Region> {
193202
return client.getIdentityCenterRegion()
194203
}
@@ -220,7 +229,7 @@ export class CodeWhispererIdentityCenterState extends BaseIdentityCenterState {
220229
221230
protected override async _startIdentityCenterSetup(): Promise<void> {
222231
const data = await this.getSubmittableDataOrThrow()
223-
return client.startIdentityCenterSetup(data.startUrl, data.region)
232+
return client.startCWIdentityCenterSetup(data.startUrl, data.region)
224233
}
225234
226235
override async isAuthConnected(): Promise<boolean> {
@@ -230,6 +239,51 @@ export class CodeWhispererIdentityCenterState extends BaseIdentityCenterState {
230239
override async showView(): Promise<void> {
231240
client.showCodeWhispererNode()
232241
}
242+
243+
override signout(): Promise<void> {
244+
return client.signoutCWIdentityCenter()
245+
}
246+
}
247+
248+
/**
249+
* In the context of the Explorer, an Identity Center connection
250+
* is not required to be active. This is due to us only needing
251+
* the connection to exist so we can grab Credentials from it.
252+
*
253+
* With this in mind, certain methods in this class don't follow
254+
* the typical connection flow.
255+
*/
256+
export class ExplorerIdentityCenterState extends BaseIdentityCenterState {
257+
override get id(): AuthFormId {
258+
return 'identityCenterExplorer'
259+
}
260+
261+
override get name(): string {
262+
return 'Resource Explorer'
263+
}
264+
265+
override async stage(): Promise<IdentityCenterStage> {
266+
// We always want to allow the user to add a new connection
267+
// for this context, so we always keep it as the start
268+
return 'START'
269+
}
270+
271+
protected override async _startIdentityCenterSetup(): Promise<void> {
272+
const data = await this.getSubmittableDataOrThrow()
273+
return client.createIdentityCenterConnection(data.startUrl, data.region)
274+
}
275+
276+
override async isAuthConnected(): Promise<boolean> {
277+
return client.isIdentityCenterExists()
278+
}
279+
280+
override async showView(): Promise<void> {
281+
client.showResourceExplorer()
282+
}
283+
284+
override signout(): Promise<void> {
285+
throw new Error('Explorer Identity Center should not use "signout functionality')
286+
}
233287
}
234288
</script>
235289
<style>

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
<script lang="ts">
22
import { CodeCatalystBuilderIdState, CodeWhispererBuilderIdState } from './manageBuilderId.vue'
33
import { CredentialsState } from './manageCredentials.vue'
4-
import { CodeWhispererIdentityCenterState } from './manageIdentityCenter.vue'
5-
import { AuthFormId } from './types'
4+
import { CodeWhispererIdentityCenterState, ExplorerIdentityCenterState } from './manageIdentityCenter.vue'
65
76
/**
87
* The state instance of all auth forms
98
*/
10-
const authFormsState: Readonly<Record<AuthFormId, any>> = {
9+
const authFormsState = {
1110
credentials: new CredentialsState() as CredentialsState,
1211
builderIdCodeWhisperer: new CodeWhispererBuilderIdState(),
1312
builderIdCodeCatalyst: new CodeCatalystBuilderIdState(),
1413
identityCenterCodeWhisperer: new CodeWhispererIdentityCenterState(),
14+
identityCenterExplorer: new ExplorerIdentityCenterState()
1515
} as const
1616
1717
export interface AuthStatus {

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,14 @@ export type AuthFormId =
88
| 'builderIdCodeWhisperer'
99
| 'builderIdCodeCatalyst'
1010
| 'identityCenterCodeWhisperer'
11+
| 'identityCenterExplorer'
12+
| 'aggregateExplorer'
1113

1214
export const AuthFormDisplayName: Record<AuthFormId, string> = {
1315
credentials: 'IAM Credentials',
1416
builderIdCodeCatalyst: 'Builder ID',
1517
builderIdCodeWhisperer: 'Builder ID',
1618
identityCenterCodeWhisperer: 'IAM Identity Center',
19+
identityCenterExplorer: 'IAM Identity Center',
20+
aggregateExplorer: ''
1721
} as const

0 commit comments

Comments
 (0)