Skip to content

Conversation

liramon1
Copy link

@liramon1 liramon1 commented Jul 8, 2025

Problem

The identity LSP does not support retrieving IAM role credentials. This forces IDE extensions to implement IAM credentials retrieval, which leads to code duplication and added complexity.

Solution

This is part of #1981 and is built on top of #1869.

This PR adds the option to assume a role and generate STS credentials if the language client requests an IAM credential using a RoleSourceProfile. After the STS credential is generated, the identity LSP caches it into .aws/cli/cache and manages its lifecycle, including expiration, invalidation, and refresh.

Note: This PR currently fails the CI pipeline because it depends on changes from aws/language-server-runtimes#599.

License

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

@liramon1 liramon1 requested a review from a team as a code owner July 8, 2025 13:56
@liramon1 liramon1 marked this pull request as draft July 9, 2025 19:52
@liramon1 liramon1 changed the base branch from main to liramon/flare-iam July 16, 2025 18:53
@liramon1 liramon1 deleted the branch aws:liramon/flare-iam-base July 16, 2025 18:57
@liramon1 liramon1 closed this Jul 16, 2025
@liramon1 liramon1 reopened this Jul 16, 2025
@liramon1 liramon1 changed the base branch from liramon/flare-iam to feature/flare-iam-base July 16, 2025 19:01
@liramon1 liramon1 marked this pull request as ready for review July 18, 2025 15:18
import { IamFlowParams, simulatePermissions } from './utils'
import { createHash } from 'crypto'

const sourceProfileRecursionMax = 5
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is recursion of source profiles even allowed? How do the CLI and SDK handle this?

Copy link
Author

@liramon1 liramon1 Jul 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, source profile recursion/chaining is allowed according to this assume-role SEP

const key = JSON.stringify({
RoleArn: params.profile.settings?.role_arn,
RoleSessionName: params.profile.settings?.role_session_name,
SerialNumber: params.profile.settings?.mfa_serial,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor nit: Keep the name as MfaSerial as it is more descriptive and immediately indicative of what this field contains. Unless the SEP prescribed this name.

Copy link
Author

@liramon1 liramon1 Jul 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The disk-cache SEP prescribed the name SerialNumber

try {
const parentCredentials = await this.getParentCredential(params)
const stsClient = new STSClient({
region: params.profile.settings?.region || 'us-east-1',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please make 'us-east-1' a const defined at the class level instead of a magic string. Also, since this logic is used in multiple places, consider wrapping getting an STS client in a function and using that throughout the code.

AwsErrorCodes,
IamCredential,
IamCredentialId,
IamCredentials,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is there an IamCredential and IamCredentials types? This will be confusing.

There is a similar naming issue with SSOToken and SsoToken I think, which was due to SSOToken being a type imported from a 3rd-party package. Is that the case here as well?

Copy link
Author

@liramon1 liramon1 Jul 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The IamCredential is supposed to be a wrapper object for an id, profile kind, and IamCredentials as suggested in this comment, but I was struggling to come up with a name for this. Any naming suggestions would be appreciated.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see:

 Check failure on line 3 in server/aws-lsp-identity/src/iam/iamProvider.ts

GitHub Actions
/ Test public NPM packages

'"@aws/language-server-runtimes/server-interface"' has no exported member named 'IamCredential'. Did you mean 'IamCredentials'?

Maybe this isn't an issue and IamCredential doesn't exist and shouldn't have been imported here? If it does exist, where is it? I couldn't find it in language-server-runtimes.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IamCredential is defined in this PR. It was added after the previous runtimes PR was approved, so it's not on the main runtimes branch yet.

const stsClient = new STSClient({ region: region || 'us-east-1', credentials: credentials })
const identity = await stsClient.send(new GetCallerIdentityCommand({}))
if (!identity.Arn) {
throw new AwsError('Caller identity ARN not found.', AwsErrorCodes.E_INVALID_PROFILE)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't an invalid profile, is there an existing or can you create a new error code specific to this error? Also, the more places an error code is reused in the code base, the less useful it becomes for debugging later. The intent of the error codes is a concrete value that can be kept overtime, even as the error messages may evolve. In the absence of error codes, people would attempt to string match on the error message which is extremely fragile.

})
}

private ignoreDoesNotExistOrThrow(error: unknown): Promise<undefined> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is in the SSO variant of this file as well. If so, consider putting in a common utils library in aws-lsp-core.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This uses a different error message and error code.

// Based on:
// https://github.com/smithy-lang/smithy-typescript/blob/main/packages/shared-ini-file-loader/src/getSSOTokenFilepath.ts
export function getStsCredentialFilepath(id: string): string {
return join(getHomeDir(), '.aws', 'flare', 'cache', `${id}.json`)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's be sure "flare" is the correct name here. I'll ping you with further details.

}

// Based on:
// https://github.com/smithy-lang/smithy-typescript/blob/main/packages/shared-ini-file-loader/src/getSSOTokenFromFile.ts
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Speaking of this, did you check the AWS SDK for JS to see if there were already functions for reading/writing STS cache files? Or see what AWS CLI code is using to do this? There might be reusable code already.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did not see any STS cache functions inside of @aws-sdk and @smithy/shared-ini-file-loader. I'm not sure if this is the primary AWS CLI credentials code, but it only references .aws for the SSO cache.


// Check if credential is still valid (not in refresh window)
if (nowMillis < expirationMillis) {
this.observability.logging.log('STS credential before refresh window. Returning current STS credential.')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these log messages and comments here consistent with what is happening? Is this how they're written in the SSO counterpart to this class? Seems a bit off.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is how it's written in the SSO counterpart. I could change the message to 'STS credential expiration is before...' and do something similar for SSO.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like it's the refresh window handling for SSO. Is that how the SEP defines handling it (e.g. try 5 minutes before at a rate of no more than every 20 or 30 seconds) for STS as well?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could not find a SEP for STS/temporary credential refresh, so I'm not sure.


let delayMs: number

if (nowMillis < expirationMillis - refreshWindowMillis) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was there a SEP specifically for STS creds refresh handling or did you just duplicate the SSO handling? If so, there is an opportunity for a base class here as there is some complicated code here that we shouldn't copy/paste reuse or it will likely lead to inconsistency/bugs in the future.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was duplicated from SSO handling. Sure, I can refactor this.

stsCredentialDetail.lastRefreshMillis = Date.now()

// Passing refresh function into here is easier than refreshing from STS cache
const newCredentials = await refreshCallback()
Copy link
Author

@liramon1 liramon1 Jul 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a bit easier to pass refreshCallback into watch instead of the STS cache with the current setup, since generateStsCredential is tightly coupled with iamProvider. Moving refreshCallback to STS cache would make it closer to the SSO implementation, but requires generateStsCredential to be a util function and uncoupled from iamProvider.

Copy link
Contributor

@floralph floralph left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given the changes being made to the existing SSO flow, be sure this functionality is all evaluated in amazon-q-eclipse as well.

AwsErrorCodes,
IamCredential,
IamCredentialId,
IamCredentials,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see:

 Check failure on line 3 in server/aws-lsp-identity/src/iam/iamProvider.ts

GitHub Actions
/ Test public NPM packages

'"@aws/language-server-runtimes/server-interface"' has no exported member named 'IamCredential'. Did you mean 'IamCredentials'?

Maybe this isn't an issue and IamCredential doesn't exist and shouldn't have been imported here? If it does exist, where is it? I couldn't find it in language-server-runtimes.


// Check if credential is still valid (not in refresh window)
if (nowMillis < expirationMillis) {
this.observability.logging.log('STS credential before refresh window. Returning current STS credential.')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like it's the refresh window handling for SSO. Is that how the SEP defines handling it (e.g. try 5 minutes before at a rate of no more than every 20 or 30 seconds) for STS as well?

} catch (e) {
this.sourceProfileRecursionCount = 0
throw e
// Get the credentials directly from the profile
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to be sure, the intent here is that if a profile has both creds defined in it and a sourceProfile listed, you would prioritize using the creds provided and ignore the sourceProfile, correct?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it should prioritize the profile creds over the sourceProfile.

return response.EvaluationResults.every(result => result.EvalDecision === 'allowed')
}

export async function checkMfaRequired(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't permissions be hard-coded as sts:AssumeRole instead of passed in? Shouldn't this call validatePermissions instead of duplicating the code? If the latter, is the function needed or should you just call validatePermissions from iamProvider.ts directly above (passing in sts:AssumeRole)?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is slightly different because it checks whether the MFA condition is present on any permission instead of whether access is granted. Hardcoding sts:AssumeRole would achieve the same outcome for now, but would not let you check MFA on other permissions if that was needed in the future.

credentials: IamCredentials,
permissions: string[],
region?: string
): Promise<SimulatePrincipalPolicyCommandOutput> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there an intended change to the return type here?

): Promise<SimulatePrincipalPolicyCommandOutput> {
// Convert the credentials into an identity
const stsClient = new STSClient({ region: region || 'us-east-1', credentials: credentials })
const stsClient = new STSClient({ region: region, credentials: credentials })
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not keep the region || defaultRegion in place? Did tsc complain since region: string is no longer optional?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought it was cleaner to make it a default argument. I can revert this if needed.

}

// Simulate permissions on the identity
const iamClient = new IAMClient({ region: region || 'us-east-1', credentials: credentials })
const iamClient = new IAMClient({ region: region, credentials: credentials })
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above about defaulting to defaultRegion handling.

@liramon1 liramon1 changed the base branch from feature/flare-iam-base to liramon/flare-iam-base July 24, 2025 22:47
@liramon1 liramon1 force-pushed the liramon/flare-iam-base branch 4 times, most recently from 4f5fc2d to d093e42 Compare July 25, 2025 18:10
@liramon1 liramon1 force-pushed the liramon/flare-iam-base branch 3 times, most recently from 82eeaf9 to e96e1c1 Compare July 28, 2025 20:07
awschristou pushed a commit to aws/language-server-runtimes that referenced this pull request Jul 29, 2025
## Problem
The IAM type changes are behind the changes in
aws/language-servers#1869 and
aws/language-servers#1846. As a result, the
language-servers PRs are unable to compile.

## Solution
This is part of #572.

- Rename validatePermissions to permissionSets and make it accept a list
of permissions instead of validating only 1 set of permissions
- Wrap credentials from getIamCredentialResult into an intermediate
object
- Add credentials override and additional error codes to
getIamCredentials
- Add mfaSerial to GetMfaSerialResult and optionalize it in
GetMfaSerialParams

<!---
    REMINDER:
    - Read CONTRIBUTING.md first.
    - Add test coverage for your changes.
    - Link to related issues/commits.
    - Testing: how did you test your changes?
    - Screenshots if applicable
-->

## License

By submitting this pull request, I confirm that my contribution is made
under the terms of the Apache 2.0 license.
liramon1 added 25 commits July 30, 2025 14:06
@liramon1 liramon1 merged commit 776a609 into aws:liramon/flare-iam-base Jul 31, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants