Skip to content

Commit 82ca492

Browse files
committed
Persist auth in memory, removing loading credentials from SDK to support SSO
1 parent 298d1a4 commit 82ca492

File tree

15 files changed

+107
-172
lines changed

15 files changed

+107
-172
lines changed

src/auth/AuthProtocol.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
} from 'vscode-languageserver';
77
import {
88
UpdateCredentialsParams,
9+
UpdateCredentialsResult,
910
ConnectionMetadata,
1011
ListProfilesParams,
1112
ListProfilesResult,
@@ -22,7 +23,9 @@ import {
2223
export const IamCredentialsUpdateRequest = Object.freeze({
2324
method: 'aws/credentials/iam/update' as const,
2425
messageDirection: MessageDirection.clientToServer,
25-
type: new ProtocolRequestType<UpdateCredentialsParams, void, never, void, void>('aws/credentials/iam/update'),
26+
type: new ProtocolRequestType<UpdateCredentialsParams, UpdateCredentialsResult, never, void, void>(
27+
'aws/credentials/iam/update',
28+
),
2629
} as const);
2730

2831
export const BearerCredentialsUpdateRequest = Object.freeze({

src/auth/AwsCredentials.ts

Lines changed: 31 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
1-
import { AwsCredentialIdentity } from '@aws-sdk/types';
21
import { DeepReadonly } from 'ts-essentials';
32
import { LspAuthHandlers } from '../protocol/LspAuthHandlers';
43
import { DefaultSettings } from '../settings/Settings';
54
import { SettingsManager } from '../settings/SettingsManager';
6-
import { parseProfile } from '../settings/SettingsParser';
7-
import { ClientMessage } from '../telemetry/ClientMessage';
85
import { LoggerFactory } from '../telemetry/LoggerFactory';
96
import { extractErrorMessage } from '../utils/Errors';
7+
import { getRegion } from '../utils/Region';
108
import { parseWithPrettyError } from '../utils/ZodErrorWrapper';
119
import {
1210
parseListProfilesResult,
@@ -30,31 +28,33 @@ import {
3028
InvalidateSsoTokenParams,
3129
InvalidateSsoTokenResult,
3230
SsoTokenChangedParams,
31+
IamCredentials,
3332
} from './AwsLspAuthTypes';
34-
import { sdkIAMCredentials } from './AwsSdkCredentialsProvider';
3533

3634
export class AwsCredentials {
3735
private readonly logger = LoggerFactory.getLogger(AwsCredentials);
38-
private profileName = DefaultSettings.profile.profile;
3936

37+
private iamCredentials?: IamCredentials;
4038
private bearerCredentials?: BearerCredentials;
4139
private connectionMetadata?: ConnectionMetadata;
4240

4341
constructor(
4442
private readonly awsHandlers: LspAuthHandlers,
4543
private readonly settingsManager: SettingsManager,
46-
private readonly clientMessage: ClientMessage,
47-
private readonly getIAMFromSdk: (
48-
profile: string,
49-
) => Promise<DeepReadonly<AwsCredentialIdentity>> = sdkIAMCredentials,
5044
) {}
5145

52-
getIAM(): Promise<DeepReadonly<AwsCredentialIdentity>> {
53-
return this.getIAMFromSdk(this.profileName);
46+
getIAM(): DeepReadonly<IamCredentials> {
47+
if (!this.iamCredentials) {
48+
throw new Error('IAM credentials not configured');
49+
}
50+
return structuredClone(this.iamCredentials);
5451
}
5552

56-
getBearer(): DeepReadonly<BearerCredentials | undefined> {
57-
return this.bearerCredentials;
53+
getBearer(): DeepReadonly<BearerCredentials> {
54+
if (!this.bearerCredentials) {
55+
throw new Error('Bearer credentials not configured');
56+
}
57+
return structuredClone(this.bearerCredentials);
5858
}
5959

6060
getConnectionMetadata(): ConnectionMetadata | undefined {
@@ -137,29 +137,28 @@ export class AwsCredentials {
137137
}
138138
}
139139

140-
handleIamCredentialsUpdate(params: UpdateCredentialsParams) {
141-
let newProfileName = DefaultSettings.profile.profile;
142-
let newRegion = DefaultSettings.profile.region;
140+
handleIamCredentialsUpdate(params: UpdateCredentialsParams): boolean {
143141
try {
144142
const { data } = parseWithPrettyError(parseUpdateCredentialsParams, params);
145143
if ('accessKeyId' in data) {
146-
const profile = parseWithPrettyError(
147-
parseProfile,
148-
{
149-
profile: data.profile,
150-
region: data.region,
151-
},
152-
DefaultSettings.profile,
153-
);
154-
155-
newProfileName = profile.profile;
156-
newRegion = profile.region;
144+
const region = getRegion(data.region);
145+
146+
this.iamCredentials = {
147+
...data,
148+
region,
149+
};
150+
151+
this.settingsManager.updateProfileSettings(data.profile, region);
152+
return true;
157153
}
154+
155+
throw new Error('Not an IAM credential');
158156
} catch (error) {
159-
this.logger.error(`Failed to update IAM profile: ${extractErrorMessage(error)}`);
160-
} finally {
161-
this.profileName = newProfileName;
162-
this.settingsManager.updateProfileSettings(newProfileName, newRegion);
157+
this.iamCredentials = undefined;
158+
159+
this.logger.error(`Failed to update IAM credentials: ${extractErrorMessage(error)}`);
160+
this.settingsManager.updateProfileSettings(DefaultSettings.profile.profile, DefaultSettings.profile.region);
161+
return false;
163162
}
164163
}
165164

@@ -183,7 +182,7 @@ export class AwsCredentials {
183182

184183
handleIamCredentialsDelete() {
185184
this.logger.info('IAM credentials deleted');
186-
this.profileName = DefaultSettings.profile.profile;
185+
this.iamCredentials = undefined;
187186
}
188187

189188
handleBearerCredentialsDelete() {

src/auth/AwsCredentialsParser.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,11 @@ const ProfileKindSchema = z.enum([
1919
]);
2020

2121
const IamCredentialsSchema = z.object({
22-
profile: z.string().optional(),
22+
profile: z.string({ message: 'Profile name is required' }),
2323
accessKeyId: z.string({ message: 'Access key ID is required' }),
2424
secretAccessKey: z.string({ message: 'Secret access key is required' }),
2525
sessionToken: z.string().optional(),
26-
region: z.string().optional(),
26+
region: z.string({ message: 'Region is required' }),
2727
});
2828

2929
const BearerCredentialsSchema = z.object({

src/auth/AwsLspAuthTypes.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
/* eslint-disable @typescript-eslint/no-empty-object-type */
22

3-
// Must be kept consistent with https://github.com/aws/language-server-runtimes
4-
export type IamCredentials = {
5-
profile?: string;
6-
accessKeyId: string;
7-
secretAccessKey: string;
8-
sessionToken?: string;
9-
region?: string;
3+
import { AwsCredentialIdentity } from '@aws-sdk/types';
4+
5+
export type IamCredentials = AwsCredentialIdentity & {
6+
profile: string;
7+
region: string;
108
};
119

1210
export type BearerCredentials = {
@@ -28,6 +26,10 @@ export type UpdateCredentialsParams = {
2826
encrypted?: boolean;
2927
};
3028

29+
export type UpdateCredentialsResult = {
30+
success: boolean;
31+
};
32+
3133
export type ProfileKind =
3234
| 'Unknown'
3335
| 'SsoTokenProfile'

src/handlers/AuthHandler.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
import { RequestHandler, NotificationHandler } from 'vscode-languageserver/node';
2-
import { UpdateCredentialsParams, SsoTokenChangedParams } from '../auth/AwsLspAuthTypes';
2+
import { UpdateCredentialsParams, UpdateCredentialsResult, SsoTokenChangedParams } from '../auth/AwsLspAuthTypes';
33
import { ServerComponents } from '../server/ServerComponents';
44

55
export function iamCredentialsUpdateHandler(
66
components: ServerComponents,
7-
): RequestHandler<UpdateCredentialsParams, void, void> {
8-
return (params: UpdateCredentialsParams) => {
9-
// AwsCredentials.handleIamCredentialsUpdate already calls settingsManager.updateProfileSettings
10-
// which will notify all subscribed components via the observable pattern
11-
components.awsCredentials.handleIamCredentialsUpdate(params);
7+
): RequestHandler<UpdateCredentialsParams, UpdateCredentialsResult, void> {
8+
return (params: UpdateCredentialsParams): UpdateCredentialsResult => {
9+
const success = components.awsCredentials.handleIamCredentialsUpdate(params);
10+
return { success };
1211
};
1312
}
1413

src/protocol/LspAuthHandlers.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
ListProfilesParams,
1818
SsoTokenChangedParams,
1919
UpdateCredentialsParams,
20+
UpdateCredentialsResult,
2021
UpdateProfileParams,
2122
} from '../auth/AwsLspAuthTypes';
2223

@@ -27,7 +28,7 @@ export class LspAuthHandlers {
2728
// ========================================
2829
// RECEIVE: Client → Server
2930
// ========================================
30-
onIamCredentialsUpdate(handler: RequestHandler<UpdateCredentialsParams, void, void>) {
31+
onIamCredentialsUpdate(handler: RequestHandler<UpdateCredentialsParams, UpdateCredentialsResult, void>) {
3132
this.connection.onRequest(IamCredentialsUpdateRequest.type, handler);
3233
}
3334

src/server/CfnExternal.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ export class CfnExternal implements Configurables, Closeable {
6363
}
6464

6565
configurables(): Configurable[] {
66-
return [this.schemaTaskManager, this.schemaRetriever, this.awsClient, this.cfnLintService, this.guardService];
66+
return [this.schemaTaskManager, this.schemaRetriever, this.cfnLintService, this.guardService];
6767
}
6868

6969
async close() {

src/server/CfnInfraCore.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,7 @@ export class CfnInfraCore implements Configurables, Closeable {
5151
this.fileContextManager = overrides.fileContextManager ?? new FileContextManager(this.documentManager);
5252

5353
this.awsCredentials =
54-
overrides.awsCredentials ??
55-
new AwsCredentials(lspComponents.authHandlers, this.settingsManager, this.clientMessage);
54+
overrides.awsCredentials ?? new AwsCredentials(lspComponents.authHandlers, this.settingsManager);
5655

5756
this.diagnosticCoordinator =
5857
overrides.diagnosticCoordinator ?? new DiagnosticCoordinator(lspComponents.diagnostics);

src/services/AwsClient.ts

Lines changed: 16 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,32 @@
11
import { CloudControlClient } from '@aws-sdk/client-cloudcontrol';
22
import { CloudFormationClient } from '@aws-sdk/client-cloudformation';
3-
import { AwsCredentialIdentity } from '@aws-sdk/types';
43
import { AwsCredentials } from '../auth/AwsCredentials';
5-
import { SettingsConfigurable, ISettingsSubscriber, SettingsSubscription } from '../settings/ISettingsSubscriber';
6-
import { DefaultSettings } from '../settings/Settings';
4+
import { IamCredentials } from '../auth/AwsLspAuthTypes';
75
import { ExtensionId, ExtensionVersion } from '../utils/ExtensionConfig';
86

9-
export class AwsClient implements SettingsConfigurable {
10-
constructor(
11-
private readonly credentialsProvider: AwsCredentials,
12-
private region: string = DefaultSettings.profile.region,
13-
private settingsSubscription?: SettingsSubscription,
14-
) {}
7+
type IamClientConfig = {
8+
region: string;
9+
credentials: IamCredentials;
10+
customUserAgent: string;
11+
};
1512

16-
configure(settingsManager: ISettingsSubscriber): void {
17-
if (this.settingsSubscription) {
18-
this.settingsSubscription.unsubscribe();
19-
}
20-
21-
this.settingsSubscription = settingsManager.subscribe('profile', (newProfile) => {
22-
this.region = newProfile.region;
23-
});
24-
}
13+
export class AwsClient {
14+
constructor(private readonly credentialsProvider: AwsCredentials) {}
2515

2616
// By default, clients will retry on throttling exceptions 3 times
27-
public async getCloudFormationClient() {
28-
return new CloudFormationClient(await this.iamClientConfig());
17+
public getCloudFormationClient() {
18+
return new CloudFormationClient(this.iamClientConfig());
2919
}
3020

31-
public async getCloudControlClient() {
32-
return new CloudControlClient(await this.iamClientConfig());
21+
public getCloudControlClient() {
22+
return new CloudControlClient(this.iamClientConfig());
3323
}
3424

35-
private async iamClientConfig(): Promise<{
36-
region: string;
37-
credentials: AwsCredentialIdentity;
38-
customUserAgent: string;
39-
}> {
40-
const data = await this.credentialsProvider.getIAM();
25+
private iamClientConfig(): IamClientConfig {
26+
const credential = this.credentialsProvider.getIAM();
4127
return {
42-
region: this.region,
43-
credentials: data,
28+
region: credential.region,
29+
credentials: this.credentialsProvider.getIAM(),
4430
customUserAgent: `${ExtensionId}/${ExtensionVersion}`,
4531
};
4632
}

src/services/CcapiService.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export class CcapiService {
1212
constructor(private readonly awsClient: AwsClient) {}
1313

1414
private async withClient<T>(request: (client: CloudControlClient) => Promise<T>): Promise<T> {
15-
const client = await this.awsClient.getCloudControlClient();
15+
const client = this.awsClient.getCloudControlClient();
1616
return await request(client);
1717
}
1818

0 commit comments

Comments
 (0)