Skip to content

Commit 0760577

Browse files
committed
fix: incorporate PR feedback
1 parent 0fc64cf commit 0760577

File tree

8 files changed

+124
-89
lines changed

8 files changed

+124
-89
lines changed
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { AwsErrorCodes, IamCredentials, Profile, ProfileKind } from '@aws/language-server-runtimes/server-interface'
2+
import { AwsError, Observability } from '@aws/lsp-core'
3+
import { ProfileStore } from '../language-server/profiles/profileService'
4+
5+
export class IamProvider {
6+
constructor(
7+
private readonly observability: Observability, // In case we need telemetry and logging in the future
8+
private readonly profileStore: ProfileStore // Will be used when assuming role with source_profile
9+
) {}
10+
11+
async getCredential(profile: Profile, callStsOnInvalidIamCredential: boolean): Promise<IamCredentials> {
12+
let credentials: IamCredentials
13+
// Get the credentials directly from the profile
14+
if (profile.kinds.includes(ProfileKind.IamCredentialsProfile)) {
15+
credentials = {
16+
accessKeyId: profile.settings!.aws_access_key_id!,
17+
secretAccessKey: profile.settings!.aws_secret_access_key!,
18+
sessionToken: profile.settings!.aws_session_token!,
19+
}
20+
} else {
21+
throw new AwsError(
22+
'Credentials could not be found for provided profile kind',
23+
AwsErrorCodes.E_INVALID_PROFILE
24+
)
25+
}
26+
27+
return credentials
28+
}
29+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { IAMClient, SimulatePrincipalPolicyCommand, SimulatePrincipalPolicyCommandOutput } from '@aws-sdk/client-iam'
2+
import { GetCallerIdentityCommand, STSClient } from '@aws-sdk/client-sts'
3+
import {
4+
AwsErrorCodes,
5+
GetMfaCodeParams,
6+
GetMfaCodeResult,
7+
IamCredentials,
8+
} from '@aws/language-server-runtimes/server-interface'
9+
import { AwsError } from '@aws/lsp-core'
10+
11+
// Simulate permissions on the identity associated with the credentials
12+
export async function simulatePermissions(
13+
credentials: IamCredentials,
14+
permissions: string[],
15+
region?: string
16+
): Promise<SimulatePrincipalPolicyCommandOutput> {
17+
// Convert the credentials into an identity
18+
const stsClient = new STSClient({ region: region || 'us-east-1', credentials: credentials })
19+
const identity = await stsClient.send(new GetCallerIdentityCommand({}))
20+
if (!identity.Arn) {
21+
throw new AwsError('Caller identity ARN not found.', AwsErrorCodes.E_INVALID_PROFILE)
22+
}
23+
24+
// Simulate permissions on the identity
25+
const iamClient = new IAMClient({ region: region || 'us-east-1', credentials: credentials })
26+
return await iamClient.send(
27+
new SimulatePrincipalPolicyCommand({
28+
PolicySourceArn: identity.Arn,
29+
ActionNames: permissions,
30+
})
31+
)
32+
}
33+
34+
export type SendGetMfaCode = (params: GetMfaCodeParams) => Promise<GetMfaCodeResult>
35+
36+
export type IamHandlers = {
37+
sendGetMfaCode: SendGetMfaCode
38+
}

server/aws-lsp-identity/src/language-server/identityServer.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import { SsoTokenAutoRefresher } from './ssoTokenAutoRefresher'
1919
import { AwsError, ServerBase } from '@aws/lsp-core'
2020
import { Features } from '@aws/language-server-runtimes/server-interface/server'
2121
import { ShowUrl, ShowMessageRequest, ShowProgress } from '../sso/utils'
22+
import { SendGetMfaCode } from '../iam/utils'
23+
import { IamProvider } from '../iam/iamProvider'
2224

2325
export class IdentityServer extends ServerBase {
2426
constructor(features: Features) {
@@ -39,6 +41,7 @@ export class IdentityServer extends ServerBase {
3941
const showMessageRequest: ShowMessageRequest = (params: ShowMessageRequestParams) =>
4042
this.features.lsp.window.showMessageRequest(params)
4143
const showProgress: ShowProgress = this.features.lsp.sendProgress
44+
const sendGetMfaCode: SendGetMfaCode = this.features.identityManagement.sendGetMfaCode
4245

4346
// Initialize dependencies
4447
const profileStore = new SharedConfigProfileStore(this.observability)
@@ -51,11 +54,14 @@ export class IdentityServer extends ServerBase {
5154

5255
const autoRefresher = new SsoTokenAutoRefresher(ssoCache, this.observability)
5356

57+
const iamProvider = new IamProvider(this.observability, profileStore)
58+
5459
const identityService = new IdentityService(
5560
profileStore,
5661
ssoCache,
5762
autoRefresher,
58-
{ showUrl, showMessageRequest, showProgress },
63+
iamProvider,
64+
{ showUrl, showMessageRequest, showProgress, sendGetMfaCode },
5965
this.getClientName(params),
6066
this.observability
6167
)

server/aws-lsp-identity/src/language-server/identityService.test.ts

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { Logging, Telemetry } from '@aws/language-server-runtimes/server-interfa
1616
import { Observability } from '@aws/lsp-core'
1717
import { STSClient } from '@aws-sdk/client-sts'
1818
import { IAMClient } from '@aws-sdk/client-iam'
19+
import { IamProvider } from '../iam/iamProvider'
1920

2021
// eslint-disable-next-line
2122
use(require('chai-as-promised'))
@@ -25,6 +26,7 @@ let sut: IdentityService
2526
let profileStore: StubbedInstance<ProfileStore>
2627
let ssoCache: StubbedInstance<SsoCache>
2728
let autoRefresher: StubbedInstance<SsoTokenAutoRefresher>
29+
let iamProvider: StubbedInstance<IamProvider>
2830
let observability: StubbedInstance<Observability>
2931
let authFlowFn: SinonSpy
3032

@@ -113,6 +115,14 @@ describe('IdentityService', () => {
113115
unwatch: undefined,
114116
}) as StubbedInstance<SsoTokenAutoRefresher>
115117

118+
iamProvider = stubInterface<IamProvider>({
119+
getCredential: Promise.resolve({
120+
accessKeyId: 'my-access-key',
121+
secretAccessKey: 'my-secret-key',
122+
sessionToken: 'my-session-token',
123+
}),
124+
})
125+
116126
authFlowFn = spy(() =>
117127
Promise.resolve({
118128
accessToken: 'my-access-token',
@@ -128,10 +138,12 @@ describe('IdentityService', () => {
128138
profileStore,
129139
ssoCache,
130140
autoRefresher,
141+
iamProvider,
131142
{
132143
showUrl: _ => {},
133144
showMessageRequest: _ => Promise.resolve({ title: 'client-response' }),
134145
showProgress: _ => Promise.resolve(),
146+
sendGetMfaCode: () => Promise.resolve({ code: 'mfa-code' }),
135147
},
136148
'My Client',
137149
observability,
@@ -317,19 +329,12 @@ describe('IdentityService', () => {
317329
})
318330

319331
describe('getIamCredential', () => {
320-
it('Can login with access key and secret key.', async () => {
321-
const actual = await sut.getIamCredential({ profileName: 'my-iam-profile' }, CancellationToken.None)
322-
323-
expect(actual.credentials.accessKeyId).to.equal('my-access-key')
324-
expect(actual.credentials.secretAccessKey).to.equal('my-secret-key')
325-
})
326-
327332
it('Can login with access key, secret key, and session token.', async () => {
328333
const actual = await sut.getIamCredential({ profileName: 'my-sts-profile' }, CancellationToken.None)
329334

330-
expect(actual.credentials.accessKeyId).to.equal('my-access-key')
331-
expect(actual.credentials.secretAccessKey).to.equal('my-secret-key')
332-
expect(actual.credentials.sessionToken).to.equal('my-session-token')
335+
expect(actual.credential.credentials.accessKeyId).to.equal('my-access-key')
336+
expect(actual.credential.credentials.secretAccessKey).to.equal('my-secret-key')
337+
expect(actual.credential.credentials.sessionToken).to.equal('my-session-token')
333338
})
334339
})
335340

server/aws-lsp-identity/src/language-server/identityService.ts

Lines changed: 14 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,12 @@ import {
99
getIamCredentialOptionsDefaults,
1010
GetSsoTokenParams,
1111
GetSsoTokenResult,
12-
IamCredentials,
1312
IamIdentityCenterSsoTokenSource,
1413
InvalidateSsoTokenParams,
1514
InvalidateSsoTokenResult,
1615
MetricEvent,
1716
SsoSession,
1817
SsoTokenSourceKind,
19-
ProfileKind,
2018
} from '@aws/language-server-runtimes/server-interface'
2119

2220
import { normalizeSettingList, ProfileStore } from './profiles/profileService'
@@ -28,45 +26,31 @@ import {
2826
throwOnInvalidSsoSession,
2927
throwOnInvalidSsoSessionName,
3028
SsoFlowParams,
29+
SsoHandlers,
3130
} from '../sso/utils'
31+
import { IamHandlers, simulatePermissions } from '../iam/utils'
3232
import { AwsError, Observability } from '@aws/lsp-core'
33-
import { GetCallerIdentityCommand, STSClient } from '@aws-sdk/client-sts'
3433
import { __ServiceException } from '@aws-sdk/client-sso-oidc/dist-types/models/SSOOIDCServiceException'
3534
import { deviceCodeFlow } from '../sso/deviceCode/deviceCodeFlow'
3635
import { SSOToken } from '@smithy/shared-ini-file-loader'
37-
import { IAMClient, SimulatePrincipalPolicyCommand, SimulatePrincipalPolicyCommandOutput } from '@aws-sdk/client-iam'
36+
import { IamProvider } from '../iam/iamProvider'
3837

3938
type SsoTokenSource = IamIdentityCenterSsoTokenSource | AwsBuilderIdSsoTokenSource
4039
type AuthFlows = Record<AuthorizationFlowKind, (params: SsoFlowParams) => Promise<SSOToken>>
40+
type Handlers = SsoHandlers & IamHandlers
4141

4242
const flows: AuthFlows = {
4343
[AuthorizationFlowKind.DeviceCode]: deviceCodeFlow,
4444
[AuthorizationFlowKind.Pkce]: authorizationCodePkceFlow,
4545
}
46-
const qPermissions = [
47-
'q:StartConversation',
48-
'q:SendMessage',
49-
'q:GetConversation',
50-
'q:ListConversations',
51-
'q:UpdateConversation',
52-
'q:DeleteConversation',
53-
'q:PassRequest',
54-
'q:StartTroubleshootingAnalysis',
55-
'q:StartTroubleshootingResolutionExplanation',
56-
'q:GetTroubleshootingResults',
57-
'q:UpdateTroubleshootingCommandResult',
58-
'q:GetIdentityMetaData',
59-
'q:GenerateCodeFromCommands',
60-
'q:UsePlugin',
61-
'codewhisperer:GenerateRecommendations',
62-
]
6346

6447
export class IdentityService {
6548
constructor(
6649
private readonly profileStore: ProfileStore,
6750
private readonly ssoCache: SsoCache,
6851
private readonly autoRefresher: SsoTokenAutoRefresher,
69-
private readonly handlers: SsoFlowParams['handlers'],
52+
private readonly iamProvider: IamProvider,
53+
private readonly handlers: Handlers,
7054
private readonly clientName: string,
7155
private readonly observability: Observability,
7256
private readonly authFlows: AuthFlows = flows
@@ -119,7 +103,7 @@ export class IdentityService {
119103
clientName: this.clientName,
120104
clientRegistration,
121105
ssoSession,
122-
handlers: this.handlers,
106+
handlers: this.handlers as Pick<Handlers, keyof SsoHandlers>,
123107
token,
124108
observability: this.observability,
125109
}
@@ -173,9 +157,7 @@ export class IdentityService {
173157
const options = { ...getIamCredentialOptionsDefaults, ...params.options }
174158

175159
token.onCancellationRequested(_ => {
176-
if (options.callStsOnInvalidIamCredential) {
177-
emitMetric('Cancelled', null)
178-
}
160+
emitMetric('Cancelled', null)
179161
})
180162

181163
// Get the profile with provided name
@@ -186,33 +168,20 @@ export class IdentityService {
186168
throw new AwsError('Profile not found.', AwsErrorCodes.E_PROFILE_NOT_FOUND)
187169
}
188170

189-
let credentials: IamCredentials
190-
// Get the credentials directly from the profile
191-
if (profile.kinds.includes(ProfileKind.IamCredentialsProfile)) {
192-
credentials = {
193-
accessKeyId: profile.settings!.aws_access_key_id!,
194-
secretAccessKey: profile.settings!.aws_secret_access_key!,
195-
sessionToken: profile.settings!.aws_session_token!,
196-
}
197-
} else {
198-
throw new AwsError('Credentials could not be found for profile', AwsErrorCodes.E_INVALID_PROFILE)
199-
}
171+
const credentials = await this.iamProvider.getCredential(profile, options.callStsOnInvalidIamCredential)
200172

201173
// Validate permissions
202-
if (options.validatePermissions) {
203-
const response = await this.simulatePermissions(credentials, qPermissions, profile.settings?.region)
174+
if (options.permissionSet.length > 0) {
175+
const response = await simulatePermissions(credentials, options.permissionSet, profile.settings?.region)
204176
if (!response?.EvaluationResults?.every(result => result.EvalDecision === 'allowed')) {
205-
throw new AwsError(
206-
`User or assumed role has insufficient permissions.`,
207-
AwsErrorCodes.E_INVALID_PROFILE
208-
)
177+
throw new AwsError(`Credentials have insufficient permissions.`, AwsErrorCodes.E_INVALID_PROFILE)
209178
}
210179
}
211180

212181
emitMetric('Succeeded')
182+
213183
return {
214-
id: profile.name,
215-
credentials: credentials,
184+
credential: { id: params.profileName, credentials: credentials },
216185
updateCredentialsParams: { data: credentials, encrypted: false },
217186
}
218187
} catch (e) {
@@ -332,27 +301,4 @@ export class IdentityService {
332301

333302
return ssoSession
334303
}
335-
336-
// Simulate permissions on the identity associated with the credentials
337-
private async simulatePermissions(
338-
credentials: IamCredentials,
339-
permissions: string[],
340-
region?: string
341-
): Promise<SimulatePrincipalPolicyCommandOutput> {
342-
// Convert the credentials into an identity
343-
const stsClient = new STSClient({ region: region || 'us-east-1', credentials: credentials })
344-
const identity = await stsClient.send(new GetCallerIdentityCommand({}))
345-
if (!identity.Arn) {
346-
throw new AwsError('Caller identity ARN not found.', AwsErrorCodes.E_INVALID_PROFILE)
347-
}
348-
349-
// Simulate permissions on the identity
350-
const iamClient = new IAMClient({ region: region || 'us-east-1', credentials: credentials })
351-
return await iamClient.send(
352-
new SimulatePrincipalPolicyCommand({
353-
PolicySourceArn: identity.Arn,
354-
ActionNames: permissions,
355-
})
356-
)
357-
}
358304
}

server/aws-lsp-identity/src/language-server/profiles/sharedConfigProfileStore.test.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,9 @@ describe('SharedConfigProfileStore', async () => {
9999
{
100100
kinds: [ProfileKind.Unknown],
101101
name: 'subsettings',
102-
settings: {},
102+
settings: {
103+
region: undefined,
104+
},
103105
},
104106
{
105107
kinds: [ProfileKind.SsoTokenProfile],
@@ -190,7 +192,9 @@ describe('SharedConfigProfileStore', async () => {
190192
{
191193
kinds: [ProfileKind.Unknown],
192194
name: 'subsettings',
193-
settings: {},
195+
settings: {
196+
region: undefined,
197+
},
194198
},
195199
{
196200
kinds: [ProfileKind.SsoTokenProfile],
@@ -286,7 +290,9 @@ describe('SharedConfigProfileStore', async () => {
286290
{
287291
kinds: [ProfileKind.Unknown],
288292
name: 'subsettings',
289-
settings: {},
293+
settings: {
294+
region: undefined,
295+
},
290296
},
291297
{
292298
kinds: ['SsoTokenProfile'],
@@ -434,7 +440,9 @@ describe('SharedConfigProfileStore', async () => {
434440
{
435441
kinds: [ProfileKind.Unknown],
436442
name: 'subsettings',
437-
settings: {},
443+
settings: {
444+
region: undefined,
445+
},
438446
},
439447
],
440448
ssoSessions: [

server/aws-lsp-identity/src/language-server/profiles/sharedConfigProfileStore.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ export class SharedConfigProfileStore implements ProfileStore {
6767
// If the profile does not match any profile type, mark it as an unknown profile
6868
if (profile.kinds.length === 0) {
6969
profile.kinds.push(ProfileKind.Unknown)
70+
// Dummy field to avoid deleting profile when loading and saving 0 changes to the profile
71+
profile.settings!['region'] = settings['region']
7072
}
7173
result.profiles.push(profile)
7274
break

0 commit comments

Comments
 (0)