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

Managed Identity and Service Principal Support #492

Merged
merged 33 commits into from
Jun 10, 2024
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
59fcaa8
init
embetten Apr 17, 2024
2c56f4e
Update unit tests
embetten Apr 18, 2024
9625343
Comment out for testing
embetten Apr 19, 2024
3dd06fe
Update BuildEndpoint Credential Provider to call MSAL Managed Identit…
embetten Apr 19, 2024
babe3dd
merge
embetten Apr 19, 2024
f5c2453
Undo non supoorted refactoring
embetten Apr 19, 2024
df05104
Add unique error message for testing purposes
embetten Apr 19, 2024
2599dd9
return MI bearer token
embetten Apr 19, 2024
24e2ea8
revert
embetten Apr 23, 2024
993314c
remove exchange comment
embetten Apr 23, 2024
9499200
Reorder token provider, add tests, formalize string
embetten Apr 23, 2024
13881fb
init
embetten Apr 24, 2024
e3bae2e
merge
embetten Apr 29, 2024
9809076
Update Credential Discovery and Creation
embetten Apr 29, 2024
3cd82f3
Added tests and resource strings
embetten May 1, 2024
34a7af5
removing non compatible syntax
embetten May 1, 2024
aeb048d
fixed another spot
embetten May 1, 2024
cba0c3a
removing duplicate invalid logging
embetten May 1, 2024
8b3edc7
fix single quote warning
embetten May 1, 2024
c15bd3a
Add tenant Id for service principal provider
embetten May 1, 2024
166f551
Remove csproj reference and fix typo
embetten May 1, 2024
0ea2659
Update BuildTaskCredProviderIsUsedError
embetten May 1, 2024
283fa0d
Fix test property group
embetten May 2, 2024
3b6caaa
Address PR Comments
embetten May 10, 2024
5511696
PR fixes Cont.
embetten May 13, 2024
ff948f1
Fix Endpoint casing
embetten May 13, 2024
606646f
Add x5C to certificate
embetten May 13, 2024
4d74c88
more pr comments
embetten May 15, 2024
0dceaff
move BuildTaskServiceEndpoint Token Provider Factory
embetten May 16, 2024
5630071
Fix tests, add licensing, revert ITokenProvidersFactory and update bu…
embetten May 17, 2024
8a4b129
move to global using
embetten May 17, 2024
83f0e39
Fix Build warnings
embetten May 20, 2024
7693d44
fix help test
embetten May 20, 2024
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 @@ -4,6 +4,8 @@
<PropertyGroup>
<LangVersion>latest</LangVersion>
<IsPackable>false</IsPackable>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<GenerateBindingRedirectsOutputType>true</GenerateBindingRedirectsOutputType>
</PropertyGroup>

<ItemGroup>
Expand All @@ -18,5 +20,4 @@
<ProjectReference Include="..\CredentialProvider.Microsoft\CredentialProvider.Microsoft.csproj" />
<ProjectReference Include="..\src\Authentication\Microsoft.Artifacts.Authentication.csproj" />
</ItemGroup>

</Project>

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
using System;
using CredentialProvider.Microsoft.Tests.CredentialProviders.Vsts;
using FluentAssertions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using NuGetCredentialProvider.Util;
using ILogger = NuGetCredentialProvider.Logging.ILogger;

namespace CredentialProvider.Microsoft.Tests.Util;

[TestClass]
public class FeedEndpointCredentialParserTests
{
private IDisposable environmentLock;
private Mock<ILogger> loggerMock;

public FeedEndpointCredentialParserTests()
{
environmentLock = EnvironmentLock.WaitAsync().Result;
loggerMock = new Mock<ILogger>();
}

[TestCleanup]
public virtual void TestCleanup()
{
Environment.SetEnvironmentVariable(EnvUtil.BuildTaskExternalEndpoints, null);
Environment.SetEnvironmentVariable(EnvUtil.EndpointCredentials, null);
environmentLock?.Dispose();
}

[TestMethod]
public void ParseFeedEndpointsJsonToDictionary_ReturnsCredentials()
{
string feedEndPointJson = "{\"endpointCredentials\":[{\"endpoint\":\"http://example.pkgs.vsts.me/_packaging/TestFeed/nuget/v3/index.json\", \"clientId\": \"testClientId\"}]}";
Environment.SetEnvironmentVariable(EnvUtil.EndpointCredentials, feedEndPointJson);

var result = FeedEndpointCredentialsParser.ParseFeedEndpointsJsonToDictionary(loggerMock.Object);

result.Count.Should().Be(1);
result["http://example.pkgs.vsts.me/_packaging/TestFeed/nuget/v3/index.json"].ClientId.Should().Be("testClientId");
}

[DataTestMethod]
[DataRow(null)]
[DataRow("")]
[DataRow(" ")]
[DataRow("invalid json")]
public void ParseFeedEndpointsJsonToDictionary_WhenInputInvalid_ReturnsEmpty(string invalidInput)
{
Environment.SetEnvironmentVariable(EnvUtil.EndpointCredentials, invalidInput);

var result = FeedEndpointCredentialsParser.ParseFeedEndpointsJsonToDictionary(loggerMock.Object);

result.Should().BeEmpty();
}

[TestMethod]
public void ParseFeedEndpointsJsonToDictionary_WithNoClientId_ReturnsEmpty()
{
string feedEndPointJson = "{\"endpointCredentials\":[{\"endpoint\":\"http://example.pkgs.vsts.me/_packaging/TestFeed/nuget/v3/index.json\"}]}";
Environment.SetEnvironmentVariable(EnvUtil.EndpointCredentials, feedEndPointJson);

var result = FeedEndpointCredentialsParser.ParseFeedEndpointsJsonToDictionary(loggerMock.Object);

result.Should().BeEmpty();
}

[TestMethod]
public void ParseFeedEndpointsJsonToDictionary_WithCertificateFilePath_ReturnsCredentials()
{
string feedEndPointJson = @"{""endpointCredentials"":[{""endpoint"":""http://example.pkgs.vsts.me/_packaging/TestFeed/nuget/v3/index.json"", ""clientId"": ""testClientId"", ""clientCertificateFilePath"": ""test\\file\\path""}]}";
Environment.SetEnvironmentVariable(EnvUtil.EndpointCredentials, feedEndPointJson);

var result = FeedEndpointCredentialsParser.ParseFeedEndpointsJsonToDictionary(loggerMock.Object);

result.Count.Should().Be(1);
result["http://example.pkgs.vsts.me/_packaging/TestFeed/nuget/v3/index.json"].ClientId.Should().Be("testClientId");
result["http://example.pkgs.vsts.me/_packaging/TestFeed/nuget/v3/index.json"].CertificateFilePath.Should().Be("test\\file\\path");
}


[TestMethod]
public void ParseFeedEndpointsJsonToDictionary_WithCertificateUnixFilePath_ReturnsCredentials()
{
string feedEndPointJson = @"{""endpointCredentials"":[{""endpoint"":""http://example.pkgs.vsts.me/_packaging/TestFeed/nuget/v3/index.json"", ""clientId"": ""testClientId"", ""clientCertificateFilePath"": ""test/file/path""}]}";
Environment.SetEnvironmentVariable(EnvUtil.EndpointCredentials, feedEndPointJson);

var result = FeedEndpointCredentialsParser.ParseFeedEndpointsJsonToDictionary(loggerMock.Object);

result.Should().NotBeNull();
result.Count.Should().Be(1);
result["http://example.pkgs.vsts.me/_packaging/TestFeed/nuget/v3/index.json"].ClientId.Should().Be("testClientId");
result["http://example.pkgs.vsts.me/_packaging/TestFeed/nuget/v3/index.json"].CertificateFilePath.Should().Be("test/file/path");
}

[TestMethod]
public void ParseFeedEndpointsJsonToDictionary_WithSubjectName_ReturnsCredentials()
{
string feedEndPointJson = @"{""endpointCredentials"":[{""endpoint"":""http://example.pkgs.vsts.me/_packaging/TestFeed/nuget/v3/index.json"", ""clientId"": ""testClientId"", ""clientCertificateSubjectName"": ""someSubjectName""}]}";
Environment.SetEnvironmentVariable(EnvUtil.EndpointCredentials, feedEndPointJson);

var result = FeedEndpointCredentialsParser.ParseFeedEndpointsJsonToDictionary(loggerMock.Object);

result.Count.Should().Be(1);
result["http://example.pkgs.vsts.me/_packaging/TestFeed/nuget/v3/index.json"].ClientId.Should().Be("testClientId");
result["http://example.pkgs.vsts.me/_packaging/TestFeed/nuget/v3/index.json"].CertificateSubjectName.Should().Be("someSubjectName");
}

[TestMethod]
public void ParseFeedEndpointsJsonToDictionary_WithCertificateUnixFilePathAndSubjectName_ReturnsEmpty()
{
string feedEndPointJson = @"{""endpointCredentials"":[{""endpoint"":""http://example.pkgs.vsts.me/_packaging/TestFeed/nuget/v3/index.json"", ""clientId"": ""testClientId"", ""clientCertificateFilePath"": ""test/file/path"", , ""clientCertificateSubjectName"": ""someSubjectName""}]}";
Environment.SetEnvironmentVariable(EnvUtil.EndpointCredentials, feedEndPointJson);

var result = FeedEndpointCredentialsParser.ParseFeedEndpointsJsonToDictionary(loggerMock.Object);

result.Should().BeEmpty();
}

[TestMethod]
public void ParseFeedEndpointsJsonToDictionary_WhenSingleQuotePresent_ReturnsEmpty()
{
string feedEndPointJson = "{'endpointCredentials':['endpoint':'http://example.pkgs.vsts.me/_packaging/TestFeed/nuget/v3/index.json', 'clientId': 'testClientId'}]}";
Environment.SetEnvironmentVariable(EnvUtil.EndpointCredentials, feedEndPointJson);

var result = FeedEndpointCredentialsParser.ParseFeedEndpointsJsonToDictionary(loggerMock.Object);

result.Should().BeEmpty();
}

[TestMethod]
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);

result.Count.Should().Be(1);
result["http://example.pkgs.vsts.me/_packaging/TestFeed/nuget/v3/index.json"].Username.Should().Be("testuser");
result["http://example.pkgs.vsts.me/_packaging/TestFeed/nuget/v3/index.json"].Password.Should().Be("testPassword");
}

[TestMethod]
public void ParseExternalFeedEndpointsJsonToDictionary_WithoutUserName_ReturnsCredentials()
{
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);

result.Count.Should().Be(1);
result["http://example.pkgs.vsts.me/_packaging/TestFeed/nuget/v3/index.json"].Username.Should().Be("VssSessionToken");
result["http://example.pkgs.vsts.me/_packaging/TestFeed/nuget/v3/index.json"].Password.Should().Be("testPassword");
}

[TestMethod]
public void ParseExternalFeedEndpointsJsonToDictionary_WithSingleQuotes_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);

result.Count.Should().Be(1);
result["http://example.pkgs.vsts.me/_packaging/TestFeed/nuget/v3/index.json"].Username.Should().Be("testuser");
result["http://example.pkgs.vsts.me/_packaging/TestFeed/nuget/v3/index.json"].Password.Should().Be("testPassword");
}

[DataTestMethod]
[DataRow(null)]
[DataRow("")]
[DataRow(" ")]
[DataRow("invalid json")]
public void ParseFeedEndpointsJsonToDictionary_WhenInvalidInput_ReturnsEmpty(string input)
{
Environment.SetEnvironmentVariable(EnvUtil.BuildTaskExternalEndpoints, input);

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

result.Should().BeEmpty();
}
}
15 changes: 15 additions & 0 deletions CredentialProvider.Microsoft/CredentialProviders/Vsts/IAuthUtil.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ public interface IAuthUtil
Task<AzDevDeploymentType> GetAzDevDeploymentType(Uri uri);

Task<Uri> GetAuthorizationEndpoint(Uri uri, CancellationToken cancellationToken);

Task<string> GetTenantIdAsync(Uri uri, CancellationToken cancellationToken);
}

public enum AzDevDeploymentType
Expand All @@ -44,6 +46,19 @@ public AuthUtil(ILogger logger)
this.logger = logger;
}

public async Task<string> GetTenantIdAsync(Uri uri, CancellationToken cancellationToken)
embetten marked this conversation as resolved.
Show resolved Hide resolved
{
var responseHeaders = await GetResponseHeadersAsync(uri, cancellationToken);

if (responseHeaders.Contains(VssResourceTenant))
{
responseHeaders.TryGetValues(VssResourceTenant, out var tenantId);
return tenantId.FirstOrDefault();
}

return null;
}

public async Task<Uri> GetAadAuthorityUriAsync(Uri uri, CancellationToken cancellationToken)
{
var environmentAuthority = EnvUtil.GetAuthorityFromEnvironment(logger);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,15 @@ public override async Task<bool> CanProvideCredentialsAsync(Uri uri)
{
// If for any reason we reach this point and any of the three build task env vars are set,
// we should not try get credentials with this cred provider.
string feedEndPointsJsonEnvVar = Environment.GetEnvironmentVariable(EnvUtil.BuildTaskExternalEndpoints);
string feedEndPointsJsonEnvVar = Environment.GetEnvironmentVariable(EnvUtil.EndpointCredentials);
string externalFeedEndPointsJsonEnvVar = Environment.GetEnvironmentVariable(EnvUtil.BuildTaskExternalEndpoints);
string uriPrefixesStringEnvVar = Environment.GetEnvironmentVariable(EnvUtil.BuildTaskUriPrefixes);
string accessTokenEnvVar = Environment.GetEnvironmentVariable(EnvUtil.BuildTaskAccessToken);

if (string.IsNullOrWhiteSpace(feedEndPointsJsonEnvVar) == false || string.IsNullOrWhiteSpace(uriPrefixesStringEnvVar) == false || string.IsNullOrWhiteSpace(accessTokenEnvVar) == false)
if (string.IsNullOrWhiteSpace(feedEndPointsJsonEnvVar) == false
|| string.IsNullOrWhiteSpace(externalFeedEndPointsJsonEnvVar) == false
embetten marked this conversation as resolved.
Show resolved Hide resolved
|| string.IsNullOrWhiteSpace(uriPrefixesStringEnvVar) == false
|| string.IsNullOrWhiteSpace(accessTokenEnvVar) == false)
{
Verbose(Resources.BuildTaskCredProviderIsUsedError);
return false;
Expand Down Expand Up @@ -116,7 +120,7 @@ public override async Task<GetAuthenticationCredentialsResponse> HandleRequestAs
Logger.Minimal(string.Format(Resources.DeviceFlowMessage, deviceCodeResult.VerificationUrl, deviceCodeResult.UserCode));

return Task.CompletedTask;
}
},
};

// Try each bearer token provider (e.g. cache, WIA, UI, DeviceCode) in order.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json;
using NuGetCredentialProvider.Logging;
using NuGetCredentialProvider.Util;

Expand All @@ -18,6 +18,12 @@ public class VstsSessionTokenClient : IVstsSessionTokenClient
{
private const string TokenScope = "vso.packaging_write vso.drop_write";

private static readonly JsonSerializerOptions options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
PropertyNameCaseInsensitive = true
};

private readonly Uri vstsUri;
private readonly string bearerToken;
private readonly IAuthUtil authUtil;
Expand Down Expand Up @@ -45,7 +51,7 @@ private HttpRequestMessage CreateRequest(Uri uri, DateTime? validTo)
};

request.Content = new StringContent(
JsonConvert.SerializeObject(tokenRequest),
JsonSerializer.Serialize(tokenRequest, options),
Encoding.UTF8,
"application/json");

Expand Down Expand Up @@ -77,7 +83,6 @@ public async Task<string> CreateSessionTokenAsync(VstsTokenType tokenType, DateT
string serializedResponse;
if (response.StatusCode == System.Net.HttpStatusCode.BadRequest)
{

request.Dispose();
response.Dispose();

Expand All @@ -97,7 +102,7 @@ public async Task<string> CreateSessionTokenAsync(VstsTokenType tokenType, DateT
serializedResponse = await response.Content.ReadAsStringAsync();
}

var responseToken = JsonConvert.DeserializeObject<VstsSessionToken>(serializedResponse);
var responseToken = JsonSerializer.Deserialize<VstsSessionToken>(serializedResponse, options);

if (validTo.Subtract(responseToken.ValidTo.Value).TotalHours > 1.0)
{
Expand Down
Loading