Skip to content

Commit e77e886

Browse files
committed
feat(identity): add support for IAM profile management (#1845)
The identity LSP was changed to load and save IAM profile kinds.
1 parent 89ae720 commit e77e886

File tree

6 files changed

+1840
-412
lines changed

6 files changed

+1840
-412
lines changed

package-lock.json

Lines changed: 1362 additions & 285 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@
4242
"typescript": "^5.8.2"
4343
},
4444
"devDependencies": {
45+
"@aws-sdk/client-iam": "^3.840.0",
46+
"@aws-sdk/client-sts": "^3.840.0",
4547
"@commitlint/cli": "^19.8.0",
4648
"@commitlint/config-conventional": "^19.8.0",
4749
"@types/ignore-walk": "^4.0.3",

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

Lines changed: 206 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ let observability: StubbedInstance<Observability>
2323
let profile1: Profile
2424
let profile2: Profile
2525
let profile3: Profile
26+
let profile4: Profile
2627
let ssoSession1: SsoSession
2728
let ssoSession2: SsoSession
2829

@@ -52,6 +53,16 @@ describe('ProfileService', async () => {
5253
},
5354
}
5455

56+
profile4 = {
57+
kinds: [ProfileKind.IamCredentialsProfile],
58+
name: 'profile4',
59+
settings: {
60+
aws_access_key_id: 'access-key',
61+
aws_secret_access_key: 'secret-key',
62+
aws_session_token: 'session-token',
63+
},
64+
}
65+
5566
ssoSession1 = {
5667
name: 'ssoSession1',
5768
settings: {
@@ -71,7 +82,7 @@ describe('ProfileService', async () => {
7182

7283
store = stubInterface<ProfileStore>({
7384
load: Promise.resolve({
74-
profiles: [profile1, profile2, profile3],
85+
profiles: [profile1, profile2, profile3, profile4],
7586
ssoSessions: [ssoSession1, ssoSession2],
7687
} satisfies ProfileData),
7788
save: Promise.resolve(),
@@ -87,7 +98,7 @@ describe('ProfileService', async () => {
8798
it('listProfiles return profiles and sso-sessions', async () => {
8899
const actual = await sut.listProfiles({})
89100

90-
expect(actual.profiles).to.be.an('array').that.has.deep.members([profile1, profile2, profile3])
101+
expect(actual.profiles).to.be.an('array').that.has.deep.members([profile1, profile2, profile3, profile4])
91102
expect(actual.ssoSessions).to.be.an('array').that.has.deep.members([ssoSession1, ssoSession2])
92103
})
93104

@@ -198,23 +209,6 @@ describe('ProfileService', async () => {
198209
expectAwsError(sut, { profile: undefined! }, AwsErrorCodes.E_INVALID_PROFILE, 'Profile required.')
199210
})
200211

201-
it('updateProfile throws on non-SSO token profile', async () => {
202-
const profile = {
203-
kinds: [ProfileKind.Unknown],
204-
name: 'profile-name',
205-
settings: {
206-
sso_session: 'sso-session-name',
207-
},
208-
}
209-
210-
await expectAwsError(
211-
sut,
212-
{ profile },
213-
AwsErrorCodes.E_INVALID_PROFILE,
214-
'Profile must be non-legacy sso-session type.'
215-
)
216-
})
217-
218212
it('updateProfile throws on no profile name', async () => {
219213
const profile = {
220214
kinds: [ProfileKind.SsoTokenProfile],
@@ -253,7 +247,7 @@ describe('ProfileService', async () => {
253247
await expectAwsError(sut, { profile }, AwsErrorCodes.E_INVALID_PROFILE, 'Sso-session name required on profile.')
254248
})
255249

256-
it('updateProfile throws on no sso-session on profile', async () => {
250+
it('updateProfile throws on no sso-session on SSO token profile', async () => {
257251
const profile = {
258252
kinds: [ProfileKind.SsoTokenProfile],
259253
name: 'profile-name',
@@ -265,6 +259,100 @@ describe('ProfileService', async () => {
265259
await expectAwsError(sut, { profile }, AwsErrorCodes.E_INVALID_PROFILE, 'Sso-session name required on profile.')
266260
})
267261

262+
it('updateProfile throws on missing access key for IamCredentialsProfile', async () => {
263+
const profile = {
264+
kinds: [ProfileKind.IamCredentialsProfile],
265+
name: 'profile-name',
266+
settings: {
267+
aws_secret_access_key: 'secret-key',
268+
},
269+
}
270+
271+
await expectAwsError(sut, { profile }, AwsErrorCodes.E_INVALID_PROFILE, 'Access key required on profile.')
272+
})
273+
274+
it('updateProfile throws on missing secret key for IamCredentialsProfile', async () => {
275+
const profile = {
276+
kinds: [ProfileKind.IamCredentialsProfile],
277+
name: 'profile-name',
278+
settings: {
279+
aws_access_key_id: 'access-key',
280+
},
281+
}
282+
283+
await expectAwsError(sut, { profile }, AwsErrorCodes.E_INVALID_PROFILE, 'Secret key required on profile.')
284+
})
285+
286+
it('updateProfile throws on missing role ARN for IamSourceProfileProfile', async () => {
287+
const profile = {
288+
kinds: [ProfileKind.IamSourceProfileProfile],
289+
name: 'profile-name',
290+
settings: {
291+
source_profile: 'source',
292+
},
293+
}
294+
295+
await expectAwsError(sut, { profile }, AwsErrorCodes.E_INVALID_PROFILE, 'Role ARN required on profile.')
296+
})
297+
298+
it('updateProfile throws on missing source profile for IamSourceProfileProfile', async () => {
299+
const profile = {
300+
kinds: [ProfileKind.IamSourceProfileProfile],
301+
name: 'profile-name',
302+
settings: {
303+
role_arn: 'role-arn',
304+
},
305+
}
306+
307+
await expectAwsError(sut, { profile }, AwsErrorCodes.E_INVALID_PROFILE, 'Source profile required on profile.')
308+
})
309+
310+
it('updateProfile throws on missing role ARN for IamCredentialSourceProfile', async () => {
311+
const profile = {
312+
kinds: [ProfileKind.IamCredentialSourceProfile],
313+
name: 'profile-name',
314+
settings: {
315+
credential_source: 'Ec2InstanceMetadata',
316+
region: 'region',
317+
},
318+
}
319+
320+
await expectAwsError(sut, { profile }, AwsErrorCodes.E_INVALID_PROFILE, 'Role ARN required on profile.')
321+
})
322+
323+
it('updateProfile throws on missing credential source for IamCredentialSourceProfile', async () => {
324+
const profile = {
325+
kinds: [ProfileKind.IamCredentialSourceProfile],
326+
name: 'profile-name',
327+
settings: {
328+
role_arn: 'role-arn',
329+
region: 'region',
330+
},
331+
}
332+
333+
await expectAwsError(
334+
sut,
335+
{ profile },
336+
AwsErrorCodes.E_INVALID_PROFILE,
337+
'Credential source required on profile.'
338+
)
339+
})
340+
341+
it('updateProfile throws on missing credential process for process profile', async () => {
342+
const profile = {
343+
kinds: [ProfileKind.IamCredentialProcessProfile],
344+
name: 'profile-name',
345+
settings: {},
346+
}
347+
348+
await expectAwsError(
349+
sut,
350+
{ profile },
351+
AwsErrorCodes.E_INVALID_PROFILE,
352+
'Credential process required on profile.'
353+
)
354+
})
355+
268356
it('updateProfile throws when profile cannot be created', async () => {
269357
const profile = {
270358
kinds: [ProfileKind.SsoTokenProfile],
@@ -411,7 +499,7 @@ describe('ProfileService', async () => {
411499
})
412500

413501
describe('profileService.DuckTypers', () => {
414-
it('profileDuckTypers.eval returns true on valid profiles', () => {
502+
it('profileDuckTypers.SsoTokenProfile.eval returns true on valid profiles', () => {
415503
const profiles = [
416504
{
417505
sso_session: 'my-sso-session',
@@ -428,7 +516,7 @@ describe('profileService.DuckTypers', () => {
428516
}
429517
})
430518

431-
it('profileDuckTypers returns false on invalid profiles', () => {
519+
it('profileDuckTypers.SsoTokenProfile.eval returns false on invalid profiles', () => {
432520
const profiles = [
433521
{
434522
SSO_session: 'my-sso-session',
@@ -484,6 +572,102 @@ describe('profileService.DuckTypers', () => {
484572
expect(actual).to.be.false
485573
}
486574
})
575+
576+
it('profileDuckTypers.IamCredentialsProfile.eval returns true on valid profiles', () => {
577+
const profiles = [
578+
{
579+
aws_access_key_id: 'access-key',
580+
aws_secret_access_key: 'secret-key',
581+
},
582+
{
583+
aws_access_key_id: 'access-key',
584+
aws_secret_access_key: 'secret-key',
585+
aws_session_token: 'session-token',
586+
},
587+
]
588+
589+
for (const profile of profiles) {
590+
const actual = profileDuckTypers.IamCredentialsProfile.eval(profile)
591+
expect(actual).to.be.true
592+
}
593+
})
594+
595+
it('profileDuckTypers.IamCredentialsProfile.eval returns false on invalid profiles', () => {
596+
const profiles = [
597+
{
598+
sso_session: 'my-sso-session',
599+
},
600+
null,
601+
{
602+
sso_account_id: '123',
603+
},
604+
]
605+
606+
for (const profile of profiles) {
607+
const actual = profileDuckTypers.IamCredentialsProfile.eval(profile as object)
608+
expect(actual).to.be.false
609+
}
610+
})
611+
612+
it('profileDuckTypers.IamSourceProfileProfile.eval returns true on valid profiles', () => {
613+
const profiles = [
614+
{
615+
role_arn: 'role-arn',
616+
source_profile: 'source-profile',
617+
},
618+
{
619+
role_arn: 'role-arn',
620+
source_profile: 'source-profile',
621+
role_session_name: 'role-session-name',
622+
mfa_serial: 'mfa-serial',
623+
},
624+
]
625+
626+
for (const profile of profiles) {
627+
const actual = profileDuckTypers.IamSourceProfileProfile.eval(profile)
628+
expect(actual).to.be.true
629+
}
630+
})
631+
632+
it('profileDuckTypers.IamCredentialSourceProfile.eval returns true on valid profiles', () => {
633+
const profiles = [
634+
{
635+
role_arn: 'role-arn',
636+
credential_source: 'credential-source',
637+
region: 'region',
638+
},
639+
{
640+
role_arn: 'role-arn',
641+
credential_source: 'credential-source',
642+
region: 'region',
643+
role_session_name: 'role-session-name',
644+
},
645+
]
646+
647+
for (const profile of profiles) {
648+
const actual = profileDuckTypers.IamCredentialSourceProfile.eval(profile)
649+
expect(actual).to.be.true
650+
}
651+
})
652+
653+
it('profileDuckTypers.IamCredentialProcessProfile.eval returns true on valid profiles', () => {
654+
const profiles = [
655+
{
656+
credential_process: 'credential-process',
657+
},
658+
{
659+
aws_access_key_id: 'access-key',
660+
aws_secret_access_key: 'secret-key',
661+
aws_session_token: 'session-token',
662+
credential_process: 'credential-process',
663+
},
664+
]
665+
666+
for (const profile of profiles) {
667+
const actual = profileDuckTypers.IamCredentialProcessProfile.eval(profile)
668+
expect(actual).to.be.true
669+
}
670+
})
487671
})
488672

489673
describe('profileService.functions', () => {

0 commit comments

Comments
 (0)