Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ public async Task HandleRequestAsync_WithInvalidBearer_ReturnsError()

mockTokenProviderFactory.Setup(x =>
x.GetAsync(It.IsAny<Uri>()))
.ReturnsAsync(new List<ITokenProvider>() {
.ReturnsAsync(new List<ITokenProvider>() {
SetUpMockManagedIdentityTokenProvider(null).Object });

var result = await vstsCredentialProvider.HandleRequestAsync(new GetAuthenticationCredentialsRequest(sourceUri, false, false, false), CancellationToken.None);
Expand Down Expand Up @@ -274,4 +274,49 @@ private static Mock<ITokenProvider> SetUpMockManagedIdentityTokenProvider(string

return mockTokenProvider;
}
}

[DataTestMethod]
[DataRow(EnvUtil.BuildTaskExternalEndpoints)]
[DataRow(EnvUtil.EndpointCredentials)]
public async Task CanProvideCredentialsPrefix_ReturnsTrueForCorrectEnvironmentVariable(string feedEndPointJsonEnvVar)
{
Uri sourceUri = new Uri(@"http://example.pkgs.vsts.me/_packaging/TestFeed/nuget/v3/index.json");
string feedEndPointJson = "{\"endpointCredentials\":[{\"endpointPrefix\":\"http://example.pkgs.vsts.me/\", \"username\": \"testUser\", \"password\":\"testToken\"}]}";

Environment.SetEnvironmentVariable(feedEndPointJsonEnvVar, feedEndPointJson);

var result = await vstsCredentialProvider.CanProvideCredentialsAsync(sourceUri);
Assert.AreEqual(true, result);
}

[TestMethod]
public async Task HandleRequestAsync_WithExternalEndpointPrefix_ReturnsSuccess()
{
Uri sourceUri = new Uri(@"http://example.pkgs.vsts.me/_packaging/TestFeed/nuget/v3/index.json");
string feedEndPointJson = "{\"endpointCredentials\":[{\"endpointPrefix\":\"http://example.pkgs.vsts.me/\", \"username\": \"testUser\", \"password\": \"testToken\"}]}";

Environment.SetEnvironmentVariable(EnvUtil.EndpointCredentials, null);
Environment.SetEnvironmentVariable(EnvUtil.BuildTaskExternalEndpoints, feedEndPointJson);

var result = await vstsCredentialProvider.HandleRequestAsync(new GetAuthenticationCredentialsRequest(sourceUri, false, false, false), CancellationToken.None);
Assert.AreEqual(MessageResponseCode.Success, result.ResponseCode);
Assert.AreEqual("testUser", result.Username);
Assert.AreEqual("testToken", result.Password);
}

[TestMethod]
public async Task HandleRequestAsync_MatchesEndpointPrefixURLWithSpaces()
{
Uri sourceUri = new Uri(@"http://example.pkgs.vsts.me/My Collection/_packaging/TestFeed/nuget/v3/index.json");

string feedEndPointJson = "{\"endpointCredentials\":[{\"endpointPrefix\":\"http://example.pkgs.vsts.me/My Collection\", \"username\": \"testUser\", \"password\":\"testToken\"}]}";

Environment.SetEnvironmentVariable(EnvUtil.BuildTaskExternalEndpoints, feedEndPointJson);

var result = await vstsCredentialProvider.HandleRequestAsync(new GetAuthenticationCredentialsRequest(sourceUri, false, false, false), CancellationToken.None);
Assert.AreEqual(MessageResponseCode.Success, result.ResponseCode);
Assert.AreEqual("testUser", result.Username);
Assert.AreEqual("testToken", result.Password);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ public void ParseExternalFeedEndpointsJsonToDictionary_ReturnsCredentials()
string feedEndPointJson = "{\"endpointCredentials\":[{\"endpoint\":\"http://example.pkgs.vsts.me/_packaging/TestFeed/nuget/v3/index.json\", \"username\": \"testuser\", \"password\": \"testPassword\"}]}";
Environment.SetEnvironmentVariable(EnvUtil.BuildTaskExternalEndpoints, feedEndPointJson);

var result = FeedEndpointCredentialsParser.ParseExternalFeedEndpointsJsonToDictionary(loggerMock.Object);
var result = FeedEndpointCredentialsParser.ParseExternalFeedEndpointsJsonToList(loggerMock.Object);

result.Count.Should().Be(1);
result["http://example.pkgs.vsts.me/_packaging/TestFeed/nuget/v3/index.json"].Username.Should().Be("testuser");
Expand All @@ -151,7 +151,7 @@ public void ParseExternalFeedEndpointsJsonToDictionary_WithoutUserName_ReturnsCr
string feedEndPointJson = "{\"endpointCredentials\":[{\"endpoint\":\"http://example.pkgs.vsts.me/_packaging/TestFeed/nuget/v3/index.json\", \"password\": \"testPassword\"}]}";
Environment.SetEnvironmentVariable(EnvUtil.BuildTaskExternalEndpoints, feedEndPointJson);

var result = FeedEndpointCredentialsParser.ParseExternalFeedEndpointsJsonToDictionary(loggerMock.Object);
var result = FeedEndpointCredentialsParser.ParseExternalFeedEndpointsJsonToList(loggerMock.Object);

result.Count.Should().Be(1);
result["http://example.pkgs.vsts.me/_packaging/TestFeed/nuget/v3/index.json"].Username.Should().Be("VssSessionToken");
Expand All @@ -164,7 +164,7 @@ public void ParseExternalFeedEndpointsJsonToDictionary_WithSingleQuotes_ReturnsC
string feedEndPointJson = "{\'endpointCredentials\':[{\'endpoint\':\'http://example.pkgs.vsts.me/_packaging/TestFeed/nuget/v3/index.json\', \'username\': \'testuser\', \'password\': \'testPassword\'}]}";
Environment.SetEnvironmentVariable(EnvUtil.BuildTaskExternalEndpoints, feedEndPointJson);

var result = FeedEndpointCredentialsParser.ParseExternalFeedEndpointsJsonToDictionary(loggerMock.Object);
var result = FeedEndpointCredentialsParser.ParseExternalFeedEndpointsJsonToList(loggerMock.Object);

result.Count.Should().Be(1);
result["http://example.pkgs.vsts.me/_packaging/TestFeed/nuget/v3/index.json"].Username.Should().Be("testuser");
Expand All @@ -180,7 +180,7 @@ public void ParseFeedEndpointsJsonToDictionary_WhenInvalidInput_ReturnsEmpty(str
{
Environment.SetEnvironmentVariable(EnvUtil.BuildTaskExternalEndpoints, input);

var result = FeedEndpointCredentialsParser.ParseExternalFeedEndpointsJsonToDictionary(loggerMock.Object);
var result = FeedEndpointCredentialsParser.ParseExternalFeedEndpointsJsonToList(loggerMock.Object);

result.Should().BeEmpty();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,13 @@ namespace NuGetCredentialProvider.CredentialProviders.VstsBuildTaskServiceEndpoi
public sealed class VstsBuildTaskServiceEndpointCredentialProvider : CredentialProviderBase
{
private Lazy<Dictionary<string, EndpointCredentials>> LazyCredentials;
private Lazy<Dictionary<string, ExternalEndpointCredentials>> LazyExternalCredentials;
private Lazy<ExternalCredentialsList> LazyExternalCredentials;
private ITokenProvidersFactory TokenProvidersFactory;
private IAuthUtil AuthUtil;

// Dictionary that maps an endpoint string to EndpointCredentials
private Dictionary<string, EndpointCredentials> Credentials => LazyCredentials.Value;
private Dictionary<string, ExternalEndpointCredentials> ExternalCredentials => LazyExternalCredentials.Value;
private ExternalCredentialsList ExternalCredentials => LazyExternalCredentials.Value;

public VstsBuildTaskServiceEndpointCredentialProvider(ILogger logger, ITokenProvidersFactory tokenProvidersFactory, IAuthUtil authUtil)
: base(logger)
Expand All @@ -34,9 +34,9 @@ public VstsBuildTaskServiceEndpointCredentialProvider(ILogger logger, ITokenProv
{
return FeedEndpointCredentialsParser.ParseFeedEndpointsJsonToDictionary(logger);
});
LazyExternalCredentials = new Lazy<Dictionary<string, ExternalEndpointCredentials>>(() =>
LazyExternalCredentials = new Lazy<ExternalCredentialsList>(() =>
{
return FeedEndpointCredentialsParser.ParseExternalFeedEndpointsJsonToDictionary(logger);
return FeedEndpointCredentialsParser.ParseExternalFeedEndpointsJsonToList(logger);
});
AuthUtil = authUtil;
}
Expand Down Expand Up @@ -66,7 +66,7 @@ public override async Task<GetAuthenticationCredentialsResponse> HandleRequestAs
Verbose(string.Format(Resources.IsRetry, request.IsRetry));

string uriString = request.Uri.AbsoluteUri;
bool externalEndpointFound = ExternalCredentials.TryGetValue(uriString, out ExternalEndpointCredentials matchingExternalEndpoint);
bool externalEndpointFound = ExternalCredentials.FindMatch(uriString, out var matchingExternalEndpoint);
if (externalEndpointFound && !string.IsNullOrWhiteSpace(matchingExternalEndpoint.Password))
{
Verbose(string.Format(Resources.BuildTaskEndpointMatchingUrlFound, uriString));
Expand Down
160 changes: 144 additions & 16 deletions CredentialProvider.Microsoft/Util/FeedEndpointCredentialsParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,153 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using ILogger = NuGetCredentialProvider.Logging.ILogger;

namespace NuGetCredentialProvider.Util;
public class ExternalEndpointCredentials

public class ExternalCredentialsConverter : Newtonsoft.Json.JsonConverter<ExternalCredentialsBase>
{
public override ExternalCredentialsBase ReadJson(JsonReader reader, Type objectType, ExternalCredentialsBase existingValue, bool hasExistingValue, Newtonsoft.Json.JsonSerializer serializer)
{
// Load the object so we can inspect which discriminator property is present
var jtoken = JObject.Load(reader);
if (jtoken is not JObject obj)
{
return null;
}

// Determine the concrete type based on endpoint / endpointPrefix and deserialize fully
if (obj.TryGetValue("endpointPrefix", StringComparison.OrdinalIgnoreCase, out var p) && p.Type == JTokenType.String)
{
var c = new ExternalEndpointPrefixCredentials();
serializer.Populate(jtoken.CreateReader(), c);
return c;
}
else if (obj.TryGetValue("endpoint", StringComparison.OrdinalIgnoreCase, out _))
{
var c = new ExternalEndpointCredentials();
serializer.Populate(jtoken.CreateReader(), c);
return c;
}

// Unknown shape; allow caller to handle validation failure
return null;
}

public override void WriteJson(JsonWriter writer, ExternalCredentialsBase value, Newtonsoft.Json.JsonSerializer serializer)
{
// Not required
throw new NotImplementedException();
}
}

[Newtonsoft.Json.JsonConverter(typeof(ExternalCredentialsConverter))]
public abstract class ExternalCredentialsBase
{
[JsonProperty("endpoint")]
public string Endpoint { get; set; }
[JsonProperty("username")]
public string Username { get; set; }
[JsonProperty("password")]
public string Password { get; set; }

internal abstract bool Validate();

internal abstract bool IsMatch(string uriString, out ExternalCredentialsBase matchingExternalEndpoint);
}

public class ExternalEndpointCredentials : ExternalCredentialsBase
{
[JsonProperty("endpoint")]
public string Endpoint { get; set; }

internal override bool IsMatch(string uriString, out ExternalCredentialsBase matchingExternalEndpoint)
{
matchingExternalEndpoint = null;
if (!Uri.TryCreate(Endpoint, UriKind.Absolute, out var endpointUri))
{
return false;
}
var url = endpointUri.AbsoluteUri;
if (url.Equals(uriString, StringComparison.OrdinalIgnoreCase))
{
matchingExternalEndpoint = this;
return true;
}
return false;
}

internal override bool Validate()
{
return Uri.TryCreate(Endpoint, UriKind.Absolute, out var endpointUri);
}

}

public class ExternalEndpointPrefixCredentials : ExternalCredentialsBase
{
[JsonProperty("endpointPrefix")]
public string EndpointPrefix { get; set; }

internal override bool IsMatch(string uriString, out ExternalCredentialsBase matchingExternalEndpoint)
{
matchingExternalEndpoint = null;
if (!Uri.TryCreate(EndpointPrefix, UriKind.Absolute, out var endpointUri))
{
return false;
}
var url = endpointUri.AbsoluteUri;
if (!url.EndsWith("/"))
{
url += '/';
}
if (uriString.StartsWith(url, StringComparison.OrdinalIgnoreCase))
{
matchingExternalEndpoint = this;
return true;
}
return false;
}

internal override bool Validate()
{
return Uri.TryCreate(EndpointPrefix, UriKind.Absolute, out var endpointUri);
}
}


public class ExternalCredentialsList : List<ExternalCredentialsBase>
{
internal bool FindMatch(string uriString, out ExternalCredentialsBase matchingExternalEndpoint)
{
matchingExternalEndpoint = null;
foreach (var credentials in this)
{
if (credentials.IsMatch(uriString, out matchingExternalEndpoint))
{
return true;
}
}
return false;
}


public ExternalCredentialsBase this[string index]
{
get
{
if (FindMatch(index, out var matchingExternalEndpoint))
{
return matchingExternalEndpoint;
}
return null;
}
}
}

public class ExternalEndpointCredentialsContainer
{
[JsonProperty("endpointCredentials")]
public ExternalEndpointCredentials[] EndpointCredentials { get; set; }
public ExternalCredentialsList EndpointCredentials { get; set; }
}

public class EndpointCredentials
Expand Down Expand Up @@ -109,12 +239,12 @@ public static Dictionary<string, EndpointCredentials> ParseFeedEndpointsJsonToDi
}
}

public static Dictionary<string, ExternalEndpointCredentials> ParseExternalFeedEndpointsJsonToDictionary(ILogger logger)
public static ExternalCredentialsList ParseExternalFeedEndpointsJsonToList(ILogger logger)
{
string feedEndpointsJson = Environment.GetEnvironmentVariable(EnvUtil.BuildTaskExternalEndpoints);
if (string.IsNullOrWhiteSpace(feedEndpointsJson))
{
return new Dictionary<string, ExternalEndpointCredentials>(StringComparer.OrdinalIgnoreCase);
return new ExternalCredentialsList();
}

try
Expand All @@ -124,15 +254,17 @@ public static Dictionary<string, ExternalEndpointCredentials> ParseExternalFeedE
{
logger.Warning(Resources.InvalidJsonWarning);
}
logger.Info(feedEndpointsJson);

Dictionary<string, ExternalEndpointCredentials> credsResult = new Dictionary<string, ExternalEndpointCredentials>(StringComparer.OrdinalIgnoreCase);
var credsResult = new ExternalCredentialsList();
ExternalEndpointCredentialsContainer endpointCredentials = JsonConvert.DeserializeObject<ExternalEndpointCredentialsContainer>(feedEndpointsJson);
if (endpointCredentials == null)
if (endpointCredentials == null || endpointCredentials.EndpointCredentials == null)
{
logger.Verbose(Resources.NoEndpointsFound);
return credsResult;
}


foreach (var credentials in endpointCredentials.EndpointCredentials)
{
if (credentials == null)
Expand All @@ -146,25 +278,21 @@ public static Dictionary<string, ExternalEndpointCredentials> ParseExternalFeedE
credentials.Username = "VssSessionToken";
}

if (!Uri.TryCreate(credentials.Endpoint, UriKind.Absolute, out var endpointUri))
if (!credentials.Validate())
{
logger.Verbose(Resources.EndpointParseFailure);
break;
}

var urlEncodedEndpoint = endpointUri.AbsoluteUri;
if (!credsResult.ContainsKey(urlEncodedEndpoint))
{
credsResult.Add(urlEncodedEndpoint, credentials);
}

credsResult.Add(credentials);
}

return credsResult;
}
catch (Exception ex)
{
logger.Verbose(string.Format(Resources.VstsBuildTaskExternalCredentialCredentialProviderError, ex));
return new Dictionary<string, ExternalEndpointCredentials>(StringComparer.OrdinalIgnoreCase);
return new ExternalCredentialsList();
}
}
}