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

Final LibGit2 credential callback changes #23770

Merged
merged 18 commits into from
Sep 22, 2017
Merged

Conversation

omus
Copy link
Member

@omus omus commented Sep 19, 2017

Should be the last round of bugfixes and minor features to the LibGit2 credential callback system before I move onto integrating git credential helpers. There are quite a few small changes in this PR including:

  • CredentialPayload's credential field is now used a temporary storage. We can safely do this without accidentally modifying cached credentials because of LibGit2 credential approval/rejection #23711
  • Load credential information from ENV only once. Allows future iterations to use user input as defaults
  • Created new isfilled function which verifies a credential is ready to be used in authentication
  • Introduced modified check which allows us to abort when all authentication methods have failed
  • Re-prompting for an SSH key will always ask for the private key to allow the user to change it
  • Introduced a way to prompt for the public key when set to an invalid file
  • All previously broken/commented out callback tests are now enabled
  • Limit number of user prompts to 3
  • Support ~ in SSH prompts for private key and public key

Additionally, at this point I think the credential_loop tests cover our testing of the SSH code paths. The ancient "SSH" testset has been removed which fixes #18403, #18306.

Part of #20725.

@omus omus added the libgit2 The libgit2 library or the LibGit2 stdlib module label Sep 19, 2017
@omus
Copy link
Member Author

omus commented Sep 19, 2017

Interesting, this code is no longer broken on Windows:

if Sys.iswindows() && LibGit2.version() >= v"0.26.0"
    # see #22681 and https://github.com/libgit2/libgit2/pull/4055
    @test_broken ex.code == LibGit2.Error.EAUTH
    @test ex.code == LibGit2.Error.ERROR
else
    @test ex.code == LibGit2.Error.EAUTH
end

I believe this may have been caused by the changes in "Set error message when auth fails" which now always sets the error message. It's possible that the LibGit2 v0.26.0 update would modify the error code if no new error message was set (working theory).

Copy link
Contributor

@rofinn rofinn left a comment

Choose a reason for hiding this comment

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

I don't see any obvious logical issues with the code, just some style and organizational suggestions.

response = Base.prompt("Private key location for '$prompt_url'",
default=privatekey)
default=creds.prvkey)
Copy link
Contributor

Choose a reason for hiding this comment

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

Same indentation issues as previous PR.

Copy link
Member Author

Choose a reason for hiding this comment

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

I've addressed this by renaming prompt_url to just url which means I can fit this keyword on a single line.

p.remaining_prompts <= 0 && return prompt_limit()
end

if !modified
Copy link
Contributor

Choose a reason for hiding this comment

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

Couldn't you just move this into the if-block above (e.g., elseif !modified)? That might make the code easier to follow.

Copy link
Member Author

Choose a reason for hiding this comment

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

I purposefully did this as I can see the if above turning into a while loop. Originally this is what was done in #20725.

function authenticate_ssh(libgit2credptr::Ptr{Ptr{Void}}, p::CredentialPayload, username_ptr)
creds = Base.get(p.credential)::SSHCredentials
modified = false
Copy link
Contributor

Choose a reason for hiding this comment

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

Some of these modified changes seem like they could simplified (e.g., You could probably just have modified = p.first_pass && isfilled(creds) at the beginning).

Copy link
Member Author

Choose a reason for hiding this comment

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

It may be better if I drop modified and just do a comparison between the initial credentials and the current credentials. I'll do some checks.

Copy link
Member Author

Choose a reason for hiding this comment

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

My idea of doing the comparison won't work. If a user were to enter the same invalid credentials twice in a row they would get an "All authentication methods have failed" error. The name modified isn't quite the right term as this just verifying that a new authentication method has been attempted.

Copy link
Member Author

Choose a reason for hiding this comment

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

I reworked this a little bit. I've ended up changing modified to revised. I think this term is better suited for the current behaviour. As for revised = p.first_pass && isfilled(creds) I've decided to keep the if block as it matches the way revised is used in the reset of the code.

if Sys.iswindows()
response = Base.winprompt(
"Please enter your credentials for '$prompt_url'", "Credentials required",
isempty(creds.user) ? p.username : creds.user; prompt_username=true)
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd be inclined to move the ternary out of the if-block and put the remaining code into an user_auth_prompt function.

end

if isempty(passphrase) && is_passphrase_required(privatekey)
# Ask for a passphrase when the private key exists and requires a passphrase
if isempty(creds.pass) && is_passphrase_required(creds.prvkey)
if Sys.iswindows()
response = Base.winprompt(
Copy link
Contributor

Choose a reason for hiding this comment

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

Move prompt logic into ssh_auth_prompt?

Copy link
Member Author

Choose a reason for hiding this comment

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

Separating these into new functions hasn't turned out well. I've decided to stick with the status quo.

@omus
Copy link
Member Author

omus commented Sep 20, 2017

Ok, I worked my way through the AppVeyor issue. As it turns out the commit "Error when credentials are not modified" corrects the broken test. In this commit we throw a EAUTH error from the callback when the credentials are not filled. I'll update the libgit2-online tests to reflect this but I also think it could be reasonable to remove the "with empty credentials" test.

The temporary credential object is independent from any cached or stored
credential until it is explicitly approved.
Credentials should be used when they are filled in and alternatives
should be tried when they are not. These changes allow use to enable
the commented out tests which use invalid credentials.

Note when prompting is disabled we now attempt to authenticate using the
default ~/.ssh/id_rsa key.
Previously if the public key file exists and the private key was
correct we could get into a scenario where we would never prompt for the
public key to be changed.
Note the prompt limit is an EAUTH error since hitting the prompt limit
should cause saved credential to be rejected.
@omus omus force-pushed the cv/libgit2-one-more-thing branch from 062b293 to 597ce0b Compare September 21, 2017 18:47
@omus
Copy link
Member Author

omus commented Sep 21, 2017

During the investigation on changing how modified worked I discovered a corner case where if prompting was disabled, ssh agent had failed, and no ENV variables were set we would fail to try the default ~/.ssh/id_rsa key. I've added a test to ensure that the default SSH key will be used without prompting.

@omus
Copy link
Member Author

omus commented Sep 21, 2017

The PR should be ready to go now. I think this should be merged rather than squashed to keep the history understandable. I've run the LibGit2 tests locally on each commit.

@omus
Copy link
Member Author

omus commented Sep 21, 2017

Found a minor issue with the LibGit2 library I'll patch yet.

Without setting an error message the message will be whatever error
message came before.
The test has been superseded by all of the new credential_loop tests.
According to the LibGit2 authentication guide
(https://libgit2.github.com/docs/guides/authentication/) the callback
is suppose to be retried if the server doesn't accept the credentials.
In the case where we use SSH and the passphrase is invalid LibGit2
would just give a generic:

  GitError(Code:ERROR, Class:SSH, Failed to authenticate SSH session: Callback returned error)
Allows the user to confirm they want to use the default private key.
During development it was noticed that the username was only being
filled in if prompting was allowed.
@omus omus force-pushed the cv/libgit2-one-more-thing branch from 597ce0b to 23d2fd2 Compare September 22, 2017 04:47
@omus
Copy link
Member Author

omus commented Sep 22, 2017

After digging into the LibGit2 issue the problem turned out to bug where the credential callback loop is aborted if the SSH passphrase is incorrect. Addressing this now makes Pkg.update much better as now you can accidentally type your SSH passphrase wrong and get re-prompted right away.

Additionally, after doing more experimenting with Pkg.update there was an issue where cached credentials were being ignored when the SSH agent was enabled. Now that these are all addressed Pkg.update authentication finally works the way I always wanted it to.

@omus
Copy link
Member Author

omus commented Sep 22, 2017

PR for the LibGit2 patch: libgit2/libgit2#4357

@omus omus merged commit def987e into master Sep 22, 2017
@StefanKarpinski StefanKarpinski deleted the cv/libgit2-one-more-thing branch September 22, 2017 14:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
libgit2 The libgit2 library or the LibGit2 stdlib module
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Fix SSH tests so they pass on buildbots, and re-enable them
2 participants