Skip to content

Commit a0a7d94

Browse files
committed
wincred: fix bug in credential matching on port nums
Fix a bug whereby we would not correctly match existing credentials when port numbers were used in the search. Additionally, actually now store the non-default port number in the target URI if there is one given. We also now always trim any trailing '/' from the end of the path of a service/target URI. Previously we erronously only trimmed the '/' from a 'pathless' URI, i.e.: https://example.com/ => https://example.com https://example.com/path/ => https://example.com/path/ This is a safe change since the `Enumerate` method that is used by both `Get` and `Remove` already matches _both_ trailing and non-trailing slashes by virtue of creating and comparing components of a `System.Uri` which does normalisation of the path. Both `Get` and `Remove` would act (eventually) to remove any bad credentials stored with a trailing slash, even if new credentials are only written out without the slash.
1 parent 3ff096f commit a0a7d94

File tree

2 files changed

+89
-6
lines changed

2 files changed

+89
-6
lines changed

src/shared/Core.Tests/Interop/Windows/WindowsCredentialManagerTests.cs

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using Xunit;
33
using GitCredentialManager.Interop.Windows;
4+
using GitCredentialManager.Interop.Windows.Native;
45

56
namespace GitCredentialManager.Tests.Interop.Windows
67
{
@@ -233,5 +234,75 @@ public void WindowsCredentialManager_RemoveUriUserInfo(string input, string expe
233234
string actual = WindowsCredentialManager.RemoveUriUserInfo(input);
234235
Assert.Equal(expected, actual);
235236
}
237+
238+
[PlatformTheory(Platforms.Windows)]
239+
[InlineData("https://example.com", null, "https://example.com", "alice", true)]
240+
[InlineData("https://example.com", "alice", "https://example.com", "alice", true)]
241+
[InlineData("https://example.com", null, "https://example.com:443", "alice", true)]
242+
[InlineData("https://example.com", "alice", "https://example.com:443", "alice", true)]
243+
[InlineData("https://example.com:1234", null, "https://example.com:1234", "alice", true)]
244+
[InlineData("https://example.com:1234", "alice", "https://example.com:1234", "alice", true)]
245+
[InlineData("https://example.com", null, "http://example.com", "alice", false)]
246+
[InlineData("https://example.com", "alice", "http://example.com", "alice", false)]
247+
[InlineData("https://example.com", "alice", "http://example.com:443", "alice", false)]
248+
[InlineData("http://example.com:443", "alice", "https://example.com", "alice", false)]
249+
[InlineData("https://example.com", "bob", "https://example.com", "alice", false)]
250+
[InlineData("https://example.com", "bob", "https://[email protected]", "bob", true)]
251+
[InlineData("https://example.com", "bob", "https://example.com", "bob", true)]
252+
[InlineData("https://example.com", "alice", "https://example.com", "ALICE", false)] // username case sensitive
253+
[InlineData("https://example.com", "alice", "https://EXAMPLE.com", "alice", true)] // host NOT case sensitive
254+
[InlineData("https://example.com/path", "alice", "https://example.com/path", "alice", true)]
255+
[InlineData("https://example.com/path", "alice", "https://example.com/PATH", "alice", true)] // path NOT case sensitive
256+
public void WindowsCredentialManager_IsMatch(
257+
string service, string account, string targetName, string userName, bool expected)
258+
{
259+
string fullTargetName = $"{WindowsCredentialManager.TargetNameLegacyGenericPrefix}{TestNamespace}:{targetName}";
260+
var win32Cred = new Win32Credential
261+
{
262+
UserName = userName,
263+
TargetName = fullTargetName
264+
};
265+
266+
var credManager = new WindowsCredentialManager(TestNamespace);
267+
268+
bool actual = credManager.IsMatch(service, account, win32Cred);
269+
270+
Assert.Equal(expected, actual);
271+
}
272+
273+
[PlatformTheory(Platforms.Windows)]
274+
[InlineData("https://example.com", null, "https://example.com")]
275+
[InlineData("https://example.com", "bob", "https://[email protected]")]
276+
[InlineData("https://example.com", "[email protected]", "https://[email protected]")] // @ in user
277+
[InlineData("https://example.com:443", null, "https://example.com")] // default port
278+
[InlineData("https://example.com:1234", null, "https://example.com:1234")]
279+
[InlineData("https://example.com/path", null, "https://example.com/path")]
280+
[InlineData("https://example.com/path/with/more/parts", null, "https://example.com/path/with/more/parts")]
281+
[InlineData("https://example.com/path/trim/", null, "https://example.com/path/trim")] // path trailing slash
282+
[InlineData("https://example.com/", null, "https://example.com")] // no path trailing slash
283+
public void WindowsCredentialManager_CreateTargetName(string service, string account, string expected)
284+
{
285+
string fullExpected = $"{TestNamespace}:{expected}";
286+
287+
var credManager = new WindowsCredentialManager(TestNamespace);
288+
289+
string actual = credManager.CreateTargetName(service, account);
290+
291+
Assert.Equal(fullExpected, actual);
292+
}
293+
294+
[PlatformTheory(Platforms.Windows)]
295+
[InlineData(TestNamespace, "https://example.com", null, $"{TestNamespace}:https://example.com")]
296+
[InlineData(null, "https://example.com", null, "https://example.com")]
297+
[InlineData("", "https://example.com", null, "https://example.com")]
298+
[InlineData(" ", "https://example.com", null, "https://example.com")]
299+
public void WindowsCredentialManager_CreateTargetName_Namespace(string @namespace, string service, string account, string expected)
300+
{
301+
var credManager = new WindowsCredentialManager(@namespace);
302+
303+
string actual = credManager.CreateTargetName(service, account);
304+
305+
Assert.Equal(expected, actual);
306+
}
236307
}
237308
}

src/shared/Core/Interop/Windows/WindowsCredentialManager.cs

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ namespace GitCredentialManager.Interop.Windows
99
{
1010
public class WindowsCredentialManager : ICredentialStore
1111
{
12-
private const string TargetNameLegacyGenericPrefix = "LegacyGeneric:target=";
12+
internal const string TargetNameLegacyGenericPrefix = "LegacyGeneric:target=";
1313

1414
private readonly string _namespace;
1515

@@ -260,7 +260,7 @@ private WindowsCredential CreateCredentialFromStructure(Win32Credential credenti
260260
return url;
261261
}
262262

263-
private bool IsMatch(string service, string account, Win32Credential credential)
263+
internal /* for testing */ bool IsMatch(string service, string account, Win32Credential credential)
264264
{
265265
// Match against the username first
266266
if (!string.IsNullOrWhiteSpace(account) &&
@@ -286,14 +286,20 @@ private bool IsMatch(string service, string account, Win32Credential credential)
286286
if (Uri.TryCreate(service, UriKind.Absolute, out Uri serviceUri) &&
287287
Uri.TryCreate(targetName, UriKind.Absolute, out Uri targetUri))
288288
{
289+
// Match scheme/protocol
290+
if (!StringComparer.OrdinalIgnoreCase.Equals(serviceUri.Scheme, targetUri.Scheme))
291+
{
292+
return false;
293+
}
294+
289295
// Match host name
290296
if (!StringComparer.OrdinalIgnoreCase.Equals(serviceUri.Host, targetUri.Host))
291297
{
292298
return false;
293299
}
294300

295301
// Match port number
296-
if (!serviceUri.IsDefaultPort && serviceUri.Port == targetUri.Port)
302+
if (serviceUri.Port != targetUri.Port)
297303
{
298304
return false;
299305
}
@@ -313,7 +319,7 @@ private bool IsMatch(string service, string account, Win32Credential credential)
313319
return false;
314320
}
315321

316-
private string CreateTargetName(string service, string account)
322+
internal /* for testing */ string CreateTargetName(string service, string account)
317323
{
318324
var serviceUri = new Uri(service, UriKind.Absolute);
319325
var sb = new StringBuilder();
@@ -339,9 +345,15 @@ private string CreateTargetName(string service, string account)
339345
sb.Append(serviceUri.Host);
340346
}
341347

342-
if (!string.IsNullOrWhiteSpace(serviceUri.AbsolutePath.TrimEnd('/')))
348+
if (!serviceUri.IsDefaultPort)
349+
{
350+
sb.AppendFormat(":{0}", serviceUri.Port);
351+
}
352+
353+
string trimmedPath = serviceUri.AbsolutePath.TrimEnd('/');
354+
if (!string.IsNullOrWhiteSpace(trimmedPath))
343355
{
344-
sb.Append(serviceUri.AbsolutePath);
356+
sb.Append(trimmedPath);
345357
}
346358

347359
return sb.ToString();

0 commit comments

Comments
 (0)