Skip to content
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

Merged
merged 38 commits into from
Jan 13, 2020
Merged

Credentials Management #888

merged 38 commits into from
Jan 13, 2020

Conversation

awschristou
Copy link
Contributor

@awschristou awschristou commented Jan 8, 2020

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:

  • added logging for Shared Credentials profiles detection and validation
  • credentials are now shown by "credential provider id" instead of profile name (example: profile:default)
  • whenever users run the "Connect to AWS" command, the Toolkit's list of available credentials providers is refreshed (eg: pulling the latest updates from the shared credentials files)
  • Credentials are cached and re-used for the duration of a toolkit session. When logging in, if a Shared Credentials Profile has been modified since it was cached, the updated version of the profile will be used instead of the cached version now.
  • updated the "Invalid Credentials" notification, and added a button guiding users to the logs
    image

Out of scope:

  • a follow-up PR will normalize all credentials related code locations
  • a follow-up PR will normalize all user facing text and all code names related to credential vs profile. "profile" terminology will be removed except where explicitly referencing profiles. Previously, the codebase wrongly used these terms rather interchangeably.

Testing

  • additional tests were added to the codebase
  • with logging on verbose level:
    • bring up the 'connect to aws' credentials list successive times. See that the logs show that the shared credentials files are only loaded once
    • bring up the 'connect to aws' credentials, then modify the shared credentials files. Bring up the credentials list again. See that the logs show that the shared credentials files were loaded twice
    • set up profiles that make a cyclical reference, and that refer to a nonexistent profile. Bring up the 'connect to aws' credentials list, and see that these profiles are not in the list (and that their validation issues are logged)
    • log in with profile X. Edit the credentials file to alter profile X. log in again with profile X in the same toolkit session. See that the logs show the profile was re-loaded and the new version of X was used to make a connection.

License

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

package.nls.json Outdated Show resolved Hide resolved
src/shared/utilities/textUtilities.ts Outdated Show resolved Hide resolved
src/credentials/activation.ts Outdated Show resolved Hide resolved
src/credentials/providers/credentialsProviderManager.ts Outdated Show resolved Hide resolved
@codecov-io
Copy link

codecov-io commented Jan 9, 2020

Codecov Report

❗ No coverage uploaded for pull request base (master@4a0e321). Click here to learn what that means.
The diff coverage is 81.25%.

Impacted file tree graph

@@            Coverage Diff            @@
##             master     #888   +/-   ##
=========================================
  Coverage          ?   57.32%           
=========================================
  Files             ?      163           
  Lines             ?     5819           
  Branches          ?      784           
=========================================
  Hits              ?     3336           
  Misses            ?     2483           
  Partials          ?        0
Impacted Files Coverage Δ
src/credentials/activation.ts 0% <0%> (ø)
src/shared/defaultAwsContextCommands.ts 0% <0%> (ø)
src/lambda/wizards/samInitWizard.ts 39.72% <0%> (ø)
...redentials/providers/credentialsProviderManager.ts 100% <100%> (ø)
src/credentials/providers/credentialsProviderId.ts 100% <100%> (ø)
...ials/providers/sharedCredentialsProviderFactory.ts 92.85% <100%> (ø)
...redentials/providers/credentialsProviderFactory.ts 100% <100%> (ø)
src/shared/utilities/textUtilities.ts 83.33% <100%> (ø)
src/credentials/credentialsStore.ts 100% <100%> (ø)
...dentials/defaultCredentialSelectionDataProvider.ts 38.96% <100%> (ø)
... and 2 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 4a0e321...ab3807e. Read the comment docs.

Copy link
Contributor

@bryceitoc9 bryceitoc9 left a 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
Comment on lines 91 to 92
"AWS.message.credentials.invalid.help": "Get Help...",
"AWS.message.credentials.invalid.logs": "View logs",
Copy link
Contributor

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"

Copy link
Contributor Author

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,
Copy link
Contributor

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.

Copy link
Contributor Author

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.

@@ -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(
Copy link
Contributor

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good catch

Copy link
Contributor Author

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.

Copy link
Contributor Author

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 Show resolved Hide resolved
})
} 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.`,
Copy link
Contributor

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.

Comment on lines +73 to +76
expectedProperties.push(
SHARED_CREDENTIAL_PROPERTIES.AWS_ACCESS_KEY_ID,
SHARED_CREDENTIAL_PROPERTIES.AWS_SECRET_ACCESS_KEY
)
Copy link
Contributor

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't follow.

Copy link
Contributor

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.

Copy link
Contributor Author

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.

Comment on lines +73 to +76
expectedProperties.push(
SHARED_CREDENTIAL_PROPERTIES.AWS_ACCESS_KEY_ID,
SHARED_CREDENTIAL_PROPERTIES.AWS_SECRET_ACCESS_KEY
)
Copy link
Contributor

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' : ''
Copy link
Contributor

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> {
Copy link
Contributor

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?

Copy link
Contributor Author

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 {
Copy link
Contributor

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?

Copy link
Contributor Author

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 () => {
Copy link
Contributor

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.

Comment on lines +169 to +172
function assertSubstringsInText(text: string | undefined, ...substrings: string[]) {
assert.ok(text)
substrings.forEach(substring => assert.notStrictEqual(text!.indexOf(substring), -1))
}
Copy link
Contributor

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.

Copy link
Contributor Author

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

Copy link
Contributor

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.

src/credentials/loginManager.ts Outdated Show resolved Hide resolved
src/credentials/loginManager.ts Outdated Show resolved Hide resolved
const credentialReaderWriter = new DefaultCredentialsFileReaderWriter()

const profileRegion = await credentialReaderWriter.getDefaultRegion(profileName)
const profileRegion = awsContext.getCredentialDefaultRegion()
Copy link
Contributor

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

Copy link
Contributor Author

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?

@@ -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, '')
Copy link
Contributor

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()
}

Copy link
Contributor Author

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

@@ -5,11 +5,16 @@

import * as AWS from 'aws-sdk'

interface CredentialsData {
credentials: AWS.Credentials
credentialsHashCode: number
Copy link
Contributor

Choose a reason for hiding this comment

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

what is this for?

Copy link
Contributor Author

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.

})
} 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.`,
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 bubbled to the user? Should they be localized?

Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor Author

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[] = []
Copy link
Contributor

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.

Copy link
Contributor Author

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)
Copy link
Contributor

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?

Copy link
Contributor Author

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(
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 just use a template literal?

Copy link
Contributor Author

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}

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.

5 participants