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

fix: #1802 confirm cached credential before rejecting #1803

Merged
merged 3 commits into from
Mar 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 25 additions & 2 deletions GVFS/GVFS.Common/Git/GitAuthentication.cs
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,27 @@ public void RejectCredentials(ITracer tracer, string credentialString)
{
lock (this.gitAuthLock)
{
string cachedCredentialAtStartOfReject = this.cachedCredentialString;
// Don't stomp a different credential
if (credentialString == this.cachedCredentialString && this.cachedCredentialString != null)
if (credentialString == cachedCredentialAtStartOfReject && cachedCredentialAtStartOfReject != null)
{
// We can't assume that the credential store's cached credential is the same as the one we have.
// Reload the credential from the store to ensure we're rejecting the correct one.
int attemptsBeforeCheckingExistingCredential = this.numberOfAttempts;
if (this.TryCallGitCredential(tracer, out string getCredentialError))
{
if (this.cachedCredentialString != cachedCredentialAtStartOfReject)
{
// If the store already had a different credential, we don't want to reject it without trying it.
this.isCachedCredentialStringApproved = false;
return;
}
}
else
{
tracer.RelatedWarning(getCredentialError);
}

// If we can we should pass the actual username/password values we used (and found to be invalid)
// to `git-credential reject` so the credential helpers can attempt to check if they're erasing
// the expected credentials, if they so choose to.
Expand Down Expand Up @@ -121,7 +139,12 @@ public void RejectCredentials(ITracer tracer, string credentialString)

this.cachedCredentialString = null;
this.isCachedCredentialStringApproved = false;
this.UpdateBackoff();

// Backoff may have already been incremented by a failure in TryCallGitCredential
if (attemptsBeforeCheckingExistingCredential == this.numberOfAttempts)
{
this.UpdateBackoff();
}
}
}
}
Expand Down
28 changes: 28 additions & 0 deletions GVFS/GVFS.UnitTests/Git/GitAuthenticationTests.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using System.Linq;
using GVFS.Common.Git;
using GVFS.Tests;
Expand Down Expand Up @@ -245,6 +246,33 @@ public void DontStoreDifferentCredentialFromCachedValue()
gitProcess.StoredCredentials.Single().Key.ShouldEqual("mock://repoUrl");
}

[TestCase]
public void RejectionShouldNotBeSentIfUnderlyingTokenHasChanged()
{
MockTracer tracer = new MockTracer();
MockGitProcess gitProcess = this.GetGitProcess();

GitAuthentication dut = new GitAuthentication(gitProcess, "mock://repoUrl");
dut.TryInitializeAndRequireAuth(tracer, out _);

// Get and store an initial value that will be cached
string authString;
dut.TryGetCredentials(tracer, out authString, out _).ShouldBeTrue();
dut.ApproveCredentials(tracer, authString);

// Change the underlying token
gitProcess.SetExpectedCommandResult(
$"{AzureDevOpsUseHttpPathString} credential fill",
() => new GitProcess.Result("username=username\r\npassword=password" + Guid.NewGuid() + "\r\n", string.Empty, GitProcess.Result.SuccessCode));

// Try and reject it. We should get a new token, but without forwarding the rejection to the
// underlying credential store
dut.RejectCredentials(tracer, authString);
dut.TryGetCredentials(tracer, out var newAuthString, out _).ShouldBeTrue();
newAuthString.ShouldNotEqual(authString);
gitProcess.CredentialRejections.ShouldBeEmpty();
}

private MockGitProcess GetGitProcess()
{
MockGitProcess gitProcess = new MockGitProcess();
Expand Down
5 changes: 3 additions & 2 deletions GVFS/GVFS.UnitTests/Mock/Git/MockGitProcess.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;

namespace GVFS.UnitTests.Mock.Git
Expand Down Expand Up @@ -91,7 +92,7 @@ protected override Result InvokeGitImpl(
return new Result(string.Empty, string.Empty, Result.GenericFailureCode);
}

Predicate<CommandInfo> commandMatchFunction =
Func<CommandInfo, bool> commandMatchFunction =
(CommandInfo commandInfo) =>
{
if (commandInfo.MatchPrefix)
Expand All @@ -104,7 +105,7 @@ protected override Result InvokeGitImpl(
}
};

CommandInfo matchedCommand = this.expectedCommandInfos.Find(commandMatchFunction);
CommandInfo matchedCommand = this.expectedCommandInfos.Last(commandMatchFunction);
matchedCommand.ShouldNotBeNull("Unexpected command: " + command);

return matchedCommand.Result();
Expand Down
5 changes: 5 additions & 0 deletions GVFS/GVFS/CommandLine/GVFSVerb.cs
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,11 @@ public static bool TrySetRequiredGitConfigSettings(Enlistment enlistment)

// Disable the builtin FS Monitor in case it was enabled globally.
{ "core.useBuiltinFSMonitor", "false" },

// Set the GCM credential method to use OAuth tokens.
// See https://github.com/git-ecosystem/git-credential-manager/blob/release/docs/configuration.md#credentialazreposcredentialtype
// for more information.
{ "credential.azreposCredentialType", "oauth" },
};

if (!TrySetConfig(enlistment, requiredSettings, isRequired: true))
Expand Down
Loading