Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
828068f
feat(identity): add support for IAM profile kinds
liramon1 Jul 10, 2025
311b455
feat(identity): add support for retrieving IAM user credentials
liramon1 Jul 10, 2025
ddc62dc
refactor: prefix IAM-related profiles with 'Iam'
liramon1 Jul 10, 2025
a63dbc1
refactor: prefix IAM-related profiles with 'Iam'
liramon1 Jul 10, 2025
a6a11b0
refactor: move profile fields into profileTypes object
liramon1 Jul 11, 2025
5fb1185
fix: add region as optional field to IamRoleSourceProfile
liramon1 Jul 11, 2025
b74a247
refactor: move MFA code retrieval into separate request
liramon1 Jul 14, 2025
108a053
fix: naming changes
liramon1 Jul 15, 2025
a731b96
chore: rename profile in unit test title
liramon1 Jul 16, 2025
71dc614
fix: sync changes with iam-profile
liramon1 Jul 16, 2025
518cbb2
chore: remove unused mfa code
liramon1 Jul 16, 2025
9570049
fix: incorporate PR feedback
liramon1 Jul 17, 2025
d9756d0
fix: remove dummy settings from unknown profiles and wrap sendGetMfaC…
liramon1 Jul 21, 2025
fd14234
feat(identity): add support for IAM profile kinds
liramon1 Jul 10, 2025
1c93d34
feat(identity): add support for assumed role credentials
liramon1 Jul 10, 2025
8cc0c7e
refactor: prefix IAM-related profiles with 'Iam'
liramon1 Jul 10, 2025
8e401ec
refactor: move profile fields into profileTypes object
liramon1 Jul 11, 2025
21dda33
refactor: move parent credentials logic into generateStsCredentials
liramon1 Jul 11, 2025
7101c99
fix: check source profile instead of original profile
liramon1 Jul 11, 2025
a8f5a6c
feat: allow any IAM profile type to be used as source profile
liramon1 Jul 14, 2025
27c78de
refactor: move MFA code retrieval into separate request
liramon1 Jul 14, 2025
f1e7c48
fix: add timeout to mfa request
liramon1 Jul 14, 2025
c959675
fix: add parameters to mfa request
liramon1 Jul 14, 2025
127fd3b
fix: incorporate PR feedback
liramon1 Jul 17, 2025
2b76c0d
refactor: move unit tests from identityService to iamProvider
liramon1 Jul 17, 2025
471c644
fix: clear MFA timeout
liramon1 Jul 17, 2025
55f58f3
fix: make STS changes compliant with disk-cache SEP
liramon1 Jul 18, 2025
228dbad
fix: remove unnecessary logs
liramon1 Jul 18, 2025
5cb8876
fix: remove dummy settings from unknown profiles and wrap sendGetMfaC…
liramon1 Jul 21, 2025
4f2bb03
fix: use profile session name if it exists
liramon1 Jul 21, 2025
d977004
fix: incorporate STS PR feedback
liramon1 Jul 22, 2025
023dac1
refactor: move common auto refresh logic into AutoRefresher class
liramon1 Jul 22, 2025
2920ee9
refactor: move recursion count from IAM provider to IAM flow params
liramon1 Jul 23, 2025
a8c23cd
fix: revert unnecessary changes
liramon1 Jul 25, 2025
2a8afee
fix: request MFA serial number from client and revert minor change
liramon1 Jul 25, 2025
a91c5f3
fix: update profile after prompting MFA serial
liramon1 Jul 29, 2025
9345d1d
fix: incorporate STS feedback
liramon1 Jul 30, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,613 changes: 269 additions & 1,344 deletions package-lock.json

Large diffs are not rendered by default.

297 changes: 297 additions & 0 deletions server/aws-lsp-identity/src/iam/iamProvider.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,297 @@
import { expect, use } from 'chai'
import { StubbedInstance, stubInterface } from 'ts-sinon'
import { ProfileData, ProfileStore } from '../language-server/profiles/profileService'
import { createStubInstance, restore, SinonStub, stub } from 'sinon'
import { CancellationToken, Profile, ProfileKind } from '@aws/language-server-runtimes/protocol'
import { Logging, Telemetry } from '@aws/language-server-runtimes/server-interface'
import { IamCredentials, Observability } from '@aws/lsp-core'
import { StsCache } from '../sts/cache/stsCache'
import { StsAutoRefresher } from '../sts/stsAutoRefresher'
import { IamProvider } from '../iam/iamProvider'
import { IamFlowParams } from './utils'
import * as iamUtils from '../iam/utils'
import { STSClient } from '@aws-sdk/client-sts'

// eslint-disable-next-line
use(require('chai-as-promised'))

let sut: IamProvider
let defaultParams: IamFlowParams
let defaultProfile: Profile
let profileStore: StubbedInstance<ProfileStore>
let stsCache: StubbedInstance<StsCache>
let stsAutoRefresher: StubbedInstance<StsAutoRefresher>
let handlers: StubbedInstance<iamUtils.IamHandlers>
let observability: StubbedInstance<Observability>
let token: StubbedInstance<CancellationToken>
let checkMfaRequiredStub: SinonStub<
[credentials: IamCredentials, permissions: string[], region?: string | undefined],
Promise<boolean>
>

describe('IamProvider', () => {
beforeEach(() => {
defaultProfile = {
kinds: [ProfileKind.Unknown],
name: 'default-profile',
}

profileStore = stubInterface<ProfileStore>({
load: Promise.resolve({
profiles: [
{
kinds: [ProfileKind.IamSourceProfileProfile],
name: 'cyclic-profile-1',
settings: {
role_arn: 'my-role-arn',
source_profile: 'cyclic-profile-1',
},
},
{
kinds: [ProfileKind.IamSourceProfileProfile],
name: 'cyclic-profile-2',
settings: {
role_arn: 'my-role-arn',
source_profile: 'cyclic-profile-3',
},
},
{
kinds: [ProfileKind.IamSourceProfileProfile],
name: 'cyclic-profile-3',
settings: {
role_arn: 'my-role-arn',
source_profile: 'cyclic-profile-2',
},
},
{
kinds: [ProfileKind.IamSourceProfileProfile],
name: 'base-profile',
settings: {
role_arn: 'my-role-arn',
source_profile: 'intermediate-profile',
},
},
{
kinds: [ProfileKind.IamSourceProfileProfile],
name: 'intermediate-profile',
settings: {
role_arn: 'my-role-arn',
source_profile: 'my-iam-profile',
},
},
{
kinds: [ProfileKind.IamCredentialsProfile],
name: 'my-iam-profile',
settings: {
aws_access_key_id: 'my-access-key',
aws_secret_access_key: 'my-secret-key',
},
},
],
ssoSessions: [],
} satisfies ProfileData),
})

stsCache = stubInterface<StsCache>({
getStsCredential: Promise.resolve(undefined),
setStsCredential: Promise.resolve(),
removeStsCredential: Promise.resolve(),
})

stsAutoRefresher = createStubInstance(StsAutoRefresher, {
watch: Promise.resolve(),
unwatch: undefined,
}) as StubbedInstance<StsAutoRefresher>

observability = stubInterface<Observability>()
observability.logging = stubInterface<Logging>()
observability.telemetry = stubInterface<Telemetry>()

handlers = stubInterface<iamUtils.IamHandlers>({
sendGetMfaCode: Promise.resolve({ code: 'mfa-code', mfaSerial: 'mfa-serial' }),
})

token = stubInterface<CancellationToken>()

defaultParams = {
profile: defaultProfile,
callStsOnInvalidIamCredential: true,
recursionCount: 0,
profileStore: profileStore,
stsCache: stsCache,
stsAutoRefresher: stsAutoRefresher,
handlers: handlers,
token: token,
observability: observability,
}

sut = new IamProvider()

checkMfaRequiredStub = stub(iamUtils, 'checkMfaRequired')
checkMfaRequiredStub.resolves(false)

stub(STSClient.prototype, 'send').resolves({
Credentials: {
AccessKeyId: 'role-access-key',
SecretAccessKey: 'role-secret-key',
SessionToken: 'role-session-token',
Expiration: new Date('2024-09-25T18:09:20.455Z'),
},
AssumedRoleUser: {
Arn: 'role-arn',
AssumedRoleId: 'role-id',
},
})
})

afterEach(() => {
restore()
})

describe('getCredential', () => {
it('Can get credentials from profile', async () => {
const profile: Profile = {
kinds: [ProfileKind.IamCredentialsProfile],
name: 'iam-profile',
settings: {
aws_access_key_id: 'access-key',
aws_secret_access_key: 'secret-key',
aws_session_token: 'session-token',
},
}
const actual = await sut.getCredential({ ...defaultParams, profile: profile })

expect(actual.credentials.accessKeyId).to.equal('access-key')
expect(actual.credentials.secretAccessKey).to.equal('secret-key')
expect(actual.credentials.sessionToken).to.equal('session-token')
})

it('Can generate credentials by assuming role.', async () => {
const profile: Profile = {
kinds: [ProfileKind.IamSourceProfileProfile],
name: 'my-role-profile',
settings: {
role_arn: 'my-role-arn',
source_profile: 'my-iam-profile',
},
}
const actual = await sut.getCredential({ ...defaultParams, profile: profile })

expect(actual.credentials.accessKeyId).to.equal('role-access-key')
expect(actual.credentials.secretAccessKey).to.equal('role-secret-key')
expect(actual.credentials.sessionToken).to.equal('role-session-token')
expect(actual.credentials.expiration?.toISOString()).to.equal('2024-09-25T18:09:20.455Z')
expect(stsAutoRefresher.watch.calledOnce).to.be.true
})

it('Can generate credentials with MFA.', async () => {
checkMfaRequiredStub.resolves(true)
const profile: Profile = {
kinds: [ProfileKind.IamSourceProfileProfile],
name: 'my-mfa-profile',
settings: {
role_arn: 'my-role-arn',
source_profile: 'my-iam-profile',
mfa_serial: 'my-device-arn',
},
}
const actual = await sut.getCredential({ ...defaultParams, profile: profile })

expect(actual.credentials.accessKeyId).to.equal('role-access-key')
expect(actual.credentials.secretAccessKey).to.equal('role-secret-key')
expect(actual.credentials.sessionToken).to.equal('role-session-token')
expect(actual.credentials.expiration?.toISOString()).to.equal('2024-09-25T18:09:20.455Z')
expect(handlers.sendGetMfaCode.calledOnce).to.be.true
})

it('Returns existing STS credential.', async () => {
const profile: Profile = {
kinds: [ProfileKind.IamSourceProfileProfile],
name: 'my-role-profile',
settings: {
role_arn: 'my-role-arn',
source_profile: 'my-iam-profile',
},
}
stsCache.getStsCredential = (() =>
Promise.resolve({
accessKeyId: 'other-access-key',
secretAccessKey: 'other-secret-key',
sessionToken: 'other-session-token',
expiration: new Date('2024-10-25T18:09:20.455Z'),
})) as any
const actual = await sut.getCredential({ ...defaultParams, profile: profile })

expect(actual.credentials.accessKeyId).to.equal('other-access-key')
expect(actual.credentials.secretAccessKey).to.equal('other-secret-key')
expect(actual.credentials.sessionToken).to.equal('other-session-token')
expect(actual.credentials.expiration?.toISOString()).to.equal('2024-10-25T18:09:20.455Z')
expect(stsAutoRefresher.watch.calledOnce).to.be.true
})

it('Throws when no STS credential cached and callStsOnInvalidIamCredential is false.', async () => {
const profile: Profile = {
kinds: [ProfileKind.IamSourceProfileProfile],
name: 'my-role-profile',
settings: {
role_arn: 'my-role-arn',
source_profile: 'my-iam-profile',
},
}
const error = await expect(
sut.getCredential({ ...defaultParams, profile: profile, callStsOnInvalidIamCredential: false })
).rejectedWith(Error)

expect(error.message).to.equal('STS credential not found.')
expect(stsAutoRefresher.watch.calledOnce).to.be.false
})

it('Can login with chained IamSourceProfileProfiles.', async () => {
const profile: Profile = {
kinds: [ProfileKind.IamSourceProfileProfile],
name: 'base-profile',
settings: {
role_arn: 'my-role-arn',
source_profile: 'intermediate-profile',
},
}
const actual = await sut.getCredential({ ...defaultParams, profile: profile })

expect(actual.credentials.accessKeyId).to.equal('role-access-key')
expect(actual.credentials.secretAccessKey).to.equal('role-secret-key')
expect(actual.credentials.sessionToken).to.equal('role-session-token')
expect(actual.credentials.expiration?.toISOString()).to.equal('2024-09-25T18:09:20.455Z')
expect(stsAutoRefresher.watch.called).to.be.true
})

it('Throws when IamSourceProfileProfile points to itself.', async () => {
const profile: Profile = {
kinds: [ProfileKind.IamSourceProfileProfile],
name: 'cyclic-profile-1',
settings: {
role_arn: 'my-role-arn',
source_profile: 'cyclic-profile-1',
},
}
const error = await expect(sut.getCredential({ ...defaultParams, profile: profile })).rejectedWith(Error)

expect(error.message).to.equal('Source profile chain exceeded max length.')
expect(stsAutoRefresher.watch.calledOnce).to.be.false
})

it('Throws when IamSourceProfileProfile form cycle.', async () => {
const profile: Profile = {
kinds: [ProfileKind.IamSourceProfileProfile],
name: 'cyclic-profile-2',
settings: {
role_arn: 'my-role-arn',
source_profile: 'cyclic-profile-3',
},
}
const error = await expect(sut.getCredential({ ...defaultParams, profile: profile })).rejectedWith(Error)

expect(error.message).to.equal('Source profile chain exceeded max length.')
expect(stsAutoRefresher.watch.calledOnce).to.be.false
})
})
})
Loading