-
Notifications
You must be signed in to change notification settings - Fork 407
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Credentials Management #888
Conversation
…etected, they are evicted from the cache and new credentials are used.
… from producing an invalid credentials notification
.changes/next-release/Feature-1c091dfc-c519-4fa1-8f1a-7ab8d7db214b.json
Outdated
Show resolved
Hide resolved
.changes/next-release/Feature-681f79db-6ef7-4172-9f5a-52b42c93ede6.json
Outdated
Show resolved
Hide resolved
Codecov Report
@@ Coverage Diff @@
## master #888 +/- ##
=========================================
Coverage ? 57.32%
=========================================
Files ? 163
Lines ? 5819
Branches ? 784
=========================================
Hits ? 3336
Misses ? 2483
Partials ? 0
Continue to review full report at Codecov.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Only through half of the PR right now but did want to provide some feedback before EOD
package.nls.json
Outdated
"AWS.message.credentials.invalid.help": "Get Help...", | ||
"AWS.message.credentials.invalid.logs": "View logs", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: I feel like we can preemptively make these generic (AWS.getHelp
, AWS.viewLogs
or something). Also, capitalize "Logs"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm going to leave these strings as credentials concerns for now, but a separate pass over all of the text to look for general-purpose opportunities is a good idea.
Capitalizing Logs
if (previousCredentialsId) { | ||
// Migrate from older Toolkits - If the last providerId isn't in the new CredentialProviderId format, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I get that this will make it more seamless for a user but I don't care to have this lying around if it's solely for that purpose. I'd say just invalidate the existing credentials and have the user log back in...it's fast enough that it shouldn't be an issue. At most, give them a one-version grace period and mark this for deletion in the next release afterwards.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can be removed after a few versions. It makes for a better experience than seeing an "invalid credentials" notification, or a phantom log-out. It's low cost, the comment explains what is going on, and git history will tell us (if necessary) when this was added.
src/credentials/credentialsStore.ts
Outdated
@@ -19,26 +24,33 @@ export class CredentialsStore { | |||
* Returns undefined if credentials are not stored for given ID | |||
*/ | |||
public async getCredentials(credentialsId: string): Promise<AWS.Credentials | undefined> { | |||
return this.credentialsCache[credentialsId] | |||
return this.credentialsCache[credentialsId]?.credentials | |||
} | |||
|
|||
/** | |||
* If credentials are not stored, the provided create function is called. Created credentials are then stored. | |||
*/ | |||
public async getCredentialsOrCreate( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: change the name to getOrCreateCredentials
; put the actions first.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfortunately, this was added in a separate PR, so the rename belongs in a separate PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The cache has been updated to treat Credentials + Hash as one object, so I added the rename as well.
src/credentials/loginManager.ts
Outdated
}) | ||
} catch (err) { | ||
getLogger().error('Error logging in', err as Error) | ||
getLogger().error( | ||
`Error trying to connect to AWS with Credentials Provider ${credentialsId}. Toolkit will now disconnect from AWS.`, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: the toolkit wasn't connected to AWS in the first place.
expectedProperties.push( | ||
SHARED_CREDENTIAL_PROPERTIES.AWS_ACCESS_KEY_ID, | ||
SHARED_CREDENTIAL_PROPERTIES.AWS_SECRET_ACCESS_KEY | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel like you can just roll the filter statement from the getMissingProperties call into here and remove the statement.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't follow.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove the getMissingProperties function and just call the array filter here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That felt more repetitive (see line 78). I figured there's more likelihood of adding similar checks in the future for other (future) property pairings.
expectedProperties.push( | ||
SHARED_CREDENTIAL_PROPERTIES.AWS_ACCESS_KEY_ID, | ||
SHARED_CREDENTIAL_PROPERTIES.AWS_SECRET_ACCESS_KEY | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove the getMissingProperties function and just call the array filter here.
|
||
export async function updateAwsSdkLoadConfigEnvironmentVariable(): Promise<void> { | ||
const configFileExists = await SystemUtilities.fileExists(getConfigFilename()) | ||
process.env.AWS_SDK_LOAD_CONFIG = configFileExists ? 'true' : '' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
= configFileExists.toString()
?
return profile | ||
} | ||
|
||
export async function updateAwsSdkLoadConfigEnvironmentVariable(): Promise<void> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need to explicitly set this value?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfortunately, we want this envvar to reflect reality as close to when we're using shared credentials as possible. Otherwise you can get into states where errors are raised due to missing files, or where profile data is ignored in newly created files.
/** | ||
* Hashes are not guaranteed to be stable across toolkit versions. | ||
*/ | ||
export function getStringHash(text: string): number { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we just use a library?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Prefer not to use a library -- we're pushing credentials profile data through this
} from '../../../credentials/providers/credentialsProviderId' | ||
import { assertThrowsError } from '../../shared/utilities/assertUtils' | ||
|
||
describe('makeCredentialsProviderIdComponents', async () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps test a profile with a colon in the name.
function assertSubstringsInText(text: string | undefined, ...substrings: string[]) { | ||
assert.ok(text) | ||
substrings.forEach(substring => assert.notStrictEqual(text!.indexOf(substring), -1)) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull this out into the generic test utils.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It makes sense to do that when there is a test that has a need for this to be shared
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm shocked that we don't have a single utils dir and (I don't think) any tests that check substrings. This stays.
…properties as a single object
const credentialReaderWriter = new DefaultCredentialsFileReaderWriter() | ||
|
||
const profileRegion = await credentialReaderWriter.getDefaultRegion(profileName) | ||
const profileRegion = awsContext.getCredentialDefaultRegion() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is this something that should be attached to the credential itself rather than a top-level on awsContext
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
awsContext contains an AwsContextCredentials object, which then backs these calls. Would it make more sense to have a single getter for that object instead of getters for each property?
src/credentials/activation.ts
Outdated
@@ -29,8 +31,17 @@ export async function loginWithMostRecentCredentials( | |||
toolkitSettings: SettingsConfiguration, | |||
loginManager: LoginManager | |||
): Promise<void> { | |||
const previousCredentialsId = toolkitSettings.readSetting(profileSettingKey, '') | |||
let previousCredentialsId = toolkitSettings.readSetting<string>(profileSettingKey, '') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: I really don't like the "create a mutable variable, apply a conditional and maybe re-assign it's value cos it's convenient" pattern - I know it's tightly scoped but I really like to avoid mutability where possible. It's a smell.
I think you're better off changing this to something like :
const previousCredentialsId = toolkitSettings.readSetting<string>(profileSettingKey, '')
if (previousCredentialsId) {
// Migrate from older Toolkits - If the last providerId isn't in the new CredentialProviderId format,
// treat it like a Shared Crdentials Provider.
const loginCredential = previousCredentialsId.indexOf(CREDENTIALS_PROVIDER_ID_SEPARATOR) === -1 ?
previousCredentialsId = makeCredentialsProviderId({
credentialType: SharedCredentialsProviderFactory.CREDENTIAL_TYPE,
credentialTypeId: previousCredentialsId
}) : previousCredentialsId
await loginManager.login(loginCredential)
} else {
await loginManager.logout()
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I completely agree. Prefer immutable where possible. Will change
src/credentials/credentialsStore.ts
Outdated
@@ -5,11 +5,16 @@ | |||
|
|||
import * as AWS from 'aws-sdk' | |||
|
|||
interface CredentialsData { | |||
credentials: AWS.Credentials | |||
credentialsHashCode: number |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what is this for?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is a hash of the contents that were used to produce the credentials.
Its purpose is to detect when things like Shared Credentials Profiles change during a toolkit session.
src/credentials/loginManager.ts
Outdated
}) | ||
} catch (err) { | ||
getLogger().error('Error logging in', err as Error) | ||
getLogger().error( | ||
`Error trying to connect to AWS with Credentials Provider ${credentialsId}. Toolkit will now disconnect from AWS.`, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are these bubbled to the user? Should they be localized?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
They're only surfaced to the user in the form of logs. Might be a good idea to surface the error to the user through the interface, but we definitely don't want to localize the logs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
They are only logged (not placed in a notification). The current approach has been to only localize contents that are sent to the UI, and not logged materials.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Take a look at the revamped notification in the PR description. The intent was to steer people to the logs if they were interested, without having to route specific details about the error into the UI.
|
||
export abstract class BaseCredentialsProviderFactory<T extends CredentialsProvider> | ||
implements CredentialsProviderFactory { | ||
protected providers: T[] = [] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is an array / list really the right data-structure here? We seem to be doing lookup-style operations mostly that point to a dictionary / map being a better choice.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The top access is a mix of "list all providers" and "get provider x" (the doc is currently open in #889 ). Collection still seems reasonable to me -- the lookups don't seem like they would have a heavy performance.
} | ||
|
||
export function makeCredentialsProviderIdComponents(credentialsProviderId: string): CredentialsProviderIdComponents { | ||
const chunks = credentialsProviderId.split(CREDENTIALS_PROVIDER_ID_SEPARATOR) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This "splitting by :
" logic seems to have leaked into a bunch of places. A "CredentialProvider" has a type and identifier which together form a 'compound-style' key that we can use to store them - but couldn't that concern be localized into a credential ID
object itself?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it is only explicit in the tests, for readability. Anywhere in the toolkit code goes through the make/decode methods.
I initially stopped short of creating a credential provider id object. Since the intent was to support additional data in the future (using the separator), its probably saving future pain to make and pass around an object now instead of a string.
I'll give this a go.
export function makeCredentialsProviderIdComponents(credentialsProviderId: string): CredentialsProviderIdComponents { | ||
const chunks = credentialsProviderId.split(CREDENTIALS_PROVIDER_ID_SEPARATOR) | ||
export function asString(credentialsProviderId: CredentialsProviderId): string { | ||
return [credentialsProviderId.credentialType, credentialsProviderId.credentialTypeId].join( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not just use a template literal?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Personal preference on this one, I find this more legible than ${credentialsProviderId.credentialType}${CREDENTIALS_PROVIDER_ID_SEPARATOR}${credentialsProviderId.credentialTypeId}
Description
This change introduces a more managed approach to implementing Credentials Providers for the toolkit. The goal is to make it easier to onboard support for new credentials types moving forward. This design is very similar to the approach taken in the AWS Toolkit for JetBrains.
The design document is in #889
Integrating this new system brought about some functional changes and bug fixes.
New Behavior:
profile:default
)Out of scope:
Testing
License
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.