Skip to content

Commit

Permalink
Refactor certificate manager to address bug-like behavior in .NET fra…
Browse files Browse the repository at this point in the history
…mework for X509Store on Linux (valid certificates NOT returned). Improve support for certificate-based identities for consistency with SAS URI support.
  • Loading branch information
brdeyo committed Jun 21, 2024
1 parent 7a19193 commit fbaf8ff
Show file tree
Hide file tree
Showing 16 changed files with 1,098 additions and 675 deletions.
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
# <img src="./website/static/img/vc-logo.svg" width="50"> Virtual Client


[![Pull Request Build](https://github.com/microsoft/VirtualClient/actions/workflows/pull-request.yml/badge.svg)](https://github.com/microsoft/VirtualClient/actions/workflows/pull-request.yml)
[![Document Build](https://github.com/microsoft/VirtualClient/actions/workflows/deploy-doc.yml/badge.svg?branch=main)](https://github.com/microsoft/VirtualClient/actions/workflows/deploy-doc.yml)
[![Document Deployment](https://github.com/microsoft/VirtualClient/actions/workflows/pages/pages-build-deployment/badge.svg)](https://github.com/microsoft/VirtualClient/actions/workflows/pages/pages-build-deployment)
[![Pull Request Builds](https://github.com/microsoft/VirtualClient/actions/workflows/pull-request.yml/badge.svg)](https://github.com/microsoft/VirtualClient/actions/workflows/pull-request.yml)
[![Documentation Builds](https://github.com/microsoft/VirtualClient/actions/workflows/deploy-doc.yml/badge.svg?branch=main)](https://github.com/microsoft/VirtualClient/actions/workflows/deploy-doc.yml)
[![Documentation Deployment Builds](https://github.com/microsoft/VirtualClient/actions/workflows/pages/pages-build-deployment/badge.svg)](https://github.com/microsoft/VirtualClient/actions/workflows/pages/pages-build-deployment)

------

The following links provide additional information on the Virtual Client project.

* [Overview](https://microsoft.github.io/VirtualClient/docs/overview/)
* [Getting Started + How to Build](https://microsoft.github.io/VirtualClient/docs/guides/0001-getting-started.md)
* [Getting Started + How to Build](https://microsoft.github.io/VirtualClient/docs/guides/getting-started)

## [Getting Started](https://microsoft.github.io/VirtualClient/docs/guides/getting-started/)

Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.15.1
1.15.2
Original file line number Diff line number Diff line change
Expand Up @@ -117,43 +117,134 @@ public void TextParserExtensionsTranslateTimeUnitAsExpected(string originalText,
}

[Test]
public void TextParserExtensionsParseVcDelimeteredParameters()
public void TextParserExtensionsParseDelimitedValuesHandlesKeyValuePairsDelimitedWithTripleCommas()
{
string example = "key1=value1,,,key2=value2,,,key3=value3";
var result = TextParsingExtensions.ParseDelimitedValues(example);

CollectionAssert.AreEqual(new Dictionary<string, string>
{
{ "key1", "value1" },
{ "key2", "value2" },
{ "key3", "value3" }
}, result);
}

[Test]
public void TextParserExtensionsParseDelimitedValuesHandlesKeyValuePairsDelimitedWithSemiColons()
{
string example = "key1=value1;key2=value2;key3=value3";
var result = TextParsingExtensions.ParseVcDelimiteredParameters(example);
var result = TextParsingExtensions.ParseDelimitedValues(example);

CollectionAssert.AreEqual(new Dictionary<string, string>
{
{ "key1", "value1" },
{ "key2", "value2" },
{ "key3", "value3" }
}, result);
}

[Test]
public void TextParserExtensionsParseDelimitedValuesHandlesKeyValuePairsDelimitedWithCommas()
{
string example = "key1=value1,key2=value2,key3=value3";
var result = TextParsingExtensions.ParseDelimitedValues(example);

CollectionAssert.AreEqual(new Dictionary<string, string>
{
{ "key1", "value1" },
{ "key2", "value2" },
{ "key3", "value3" }
}, result);
}

[Test]
public void TextParserExtensionsParseDelimitedValuesHandlesKeyValuePairsThatHaveValuesContainingDelimiters()
{
string example = "key1=v1a,v1b,v1c;key2=value2;key3=v3a,v3b";
var result = TextParsingExtensions.ParseDelimitedValues(example);

string exampleWithSemiColon = "key1=v1a;v1b,v1c;key2=value2;key3=v3a;v3b";
result = TextParsingExtensions.ParseVcDelimiteredParameters(exampleWithSemiColon);
CollectionAssert.AreEqual(new Dictionary<string, string>
{
{ "key1", "v1a;v1b,v1c" },
{ "key1", "v1a,v1b,v1c" },
{ "key2", "value2" },
{ "key3", "v3a;v3b" }
{ "key3", "v3a,v3b" }
}, result);

string exampleWithEqualSign = "key1=v1a;v1b,v1c,,,key2=value2a=value2b,,,key3=v3a;v3b";
result = TextParsingExtensions.ParseVcDelimiteredParameters(exampleWithEqualSign);
example = "key1=v1a,v1b,v1c,,,key2=value2,,,key3=v3a,v3b";
result = TextParsingExtensions.ParseDelimitedValues(example);

CollectionAssert.AreEqual(new Dictionary<string, string>
{
{ "key1", "v1a;v1b,v1c" },
{ "key2", "value2a=value2b" },
{ "key3", "v3a;v3b" }
{ "key1", "v1a,v1b,v1c" },
{ "key2", "value2" },
{ "key3", "v3a,v3b" }
}, result);

string complexExample = "key1=v1a;v1b,v1 c;key2=value2;key3=v 3 a;;v3b;key4=v4a;v4b;v4c;;;v4d";
result = TextParsingExtensions.ParseVcDelimiteredParameters(complexExample);
example = "key1=v1a;v1b;v1c,,,key2=value2,,,key3=v3a;v3b";
result = TextParsingExtensions.ParseDelimitedValues(example);

CollectionAssert.AreEqual(new Dictionary<string, string>
{
{ "key1", "v1a;v1b,v1 c" },
{ "key1", "v1a;v1b;v1c" },
{ "key2", "value2" },
{ "key3", "v 3 a;;v3b" },
{ "key4", "v4a;v4b;v4c;;;v4d" }
{ "key3", "v3a;v3b" }
}, result);
}

[Test]
public void TextParserExtensionsParseDelimitedValuesHandlesConnectionStringsWithCertificateThumbprint()
{
string example =
"CertificateThumbprint=a7b126e40c1f80b40c1d8b2e1d27ac47da6a456f;ClientId=924796e9-a608-483f-9a9c-4f96dB865123;" +
"TenantId=fd456aa1-af19-48d2-8dbf-caeea9111712;EndpointUrl=https://any.blob.core.windows.net";

var result = TextParsingExtensions.ParseDelimitedValues(example);

CollectionAssert.AreEqual(new Dictionary<string, string>
{
{ "CertificateThumbprint", "a7b126e40c1f80b40c1d8b2e1d27ac47da6a456f" },
{ "ClientId", "924796e9-a608-483f-9a9c-4f96dB865123" },
{ "TenantId", "fd456aa1-af19-48d2-8dbf-caeea9111712" },
{ "EndpointUrl", "https://any.blob.core.windows.net"}
}, result);
}

[Test]
public void TextParserExtensionsParseDelimitedValuesHandlesConnectionStringsWithCertificateIssuerAndSubject()
{
string example =
"CertificateIssuer=Any Infra CA 01;CertificateSubject=any.service.azure.com;" +
"ClientId=924796e9-a608-483f-9a9c-4f96dB865123;TenantId=fd456aa1-af19-48d2-8dbf-caeea9111712;EndpointUrl=https://any.blob.core.windows.net";

var result = TextParsingExtensions.ParseDelimitedValues(example);

CollectionAssert.AreEqual(new Dictionary<string, string>
{
{ "CertificateIssuer", "Any Infra CA 01" },
{ "CertificateSubject", "any.service.azure.com" },
{ "ClientId", "924796e9-a608-483f-9a9c-4f96dB865123" },
{ "TenantId", "fd456aa1-af19-48d2-8dbf-caeea9111712" },
{ "EndpointUrl", "https://any.blob.core.windows.net"}
}, result);
}

[Test]
public void TextParserExtensionsParseDelimitedValuesHandlesConnectionStringsWithCertificateIssuerAndSubjectDistinguishedNames()
{
string example =
"CertificateIssuer=CN=Any Infra CA 01, DC=ABC, DC=COM;CertificateSubject=CN=any.service.azure.com;" +
"ClientId=924796e9-a608-483f-9a9c-4f96dB865123;TenantId=fd456aa1-af19-48d2-8dbf-caeea9111712;EndpointUrl=https://any.blob.core.windows.net";

var result = TextParsingExtensions.ParseDelimitedValues(example);

CollectionAssert.AreEqual(new Dictionary<string, string>
{
{ "CertificateIssuer", "CN=Any Infra CA 01, DC=ABC, DC=COM" },
{ "CertificateSubject", "CN=any.service.azure.com" },
{ "ClientId", "924796e9-a608-483f-9a9c-4f96dB865123" },
{ "TenantId", "fd456aa1-af19-48d2-8dbf-caeea9111712" },
{ "EndpointUrl", "https://any.blob.core.windows.net"}
}, result);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ public static class TextParsingExtensions
/// </summary>
public static readonly string EmailRegex = @"[\w\-\.]+@([\w -]+\.)+[\w-]{2,}";

private static readonly Regex CommaDelimitedExpression = new Regex(",", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex SemiColonDelimitedExpression = new Regex(";", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private static readonly Regex TripleCommaDelimitedExpression = new Regex(",,,", RegexOptions.Compiled | RegexOptions.IgnoreCase);

/// <summary>
/// Remove rows that matches the regex.
/// </summary>
Expand Down Expand Up @@ -108,48 +112,52 @@ public static IDictionary<string, string> Sectionize(string text, Regex delimite
/// Sectionize raw text into sections based on regex. First line of each section will become section key!
/// </summary>
/// <param name="text">Raw text.</param>
public static IDictionary<string, IConvertible> ParseVcDelimiteredParameters(string text)
public static IDictionary<string, IConvertible> ParseDelimitedValues(string text)
{
IDictionary<string, IConvertible> delimitedValues = new Dictionary<string, IConvertible>(StringComparer.OrdinalIgnoreCase);

if (text.Contains(",,,"))
// Priority of Delimiters
// 1) Triple-Comma Delimited (e.g. key1=value1,,,key2=value2)
// 2) Semi-Colon Delimited (e.g. key1=value1;key2=value2)
// 3) Comma Delimited (e.g. key1=value1,key2=value2)

string[] delimitedKeyValuePairs = null;

if (TextParsingExtensions.TripleCommaDelimitedExpression.IsMatch(text))
{
// Triple-comma delimiting
delimitedKeyValuePairs = TextParsingExtensions.TripleCommaDelimitedExpression.Split(text);
}
else if (TextParsingExtensions.SemiColonDelimitedExpression.IsMatch(text))
{
// Semi-Colon delimiting
delimitedKeyValuePairs = TextParsingExtensions.SemiColonDelimitedExpression.Split(text);
}
else if (TextParsingExtensions.CommaDelimitedExpression.IsMatch(text))
{
// If the list contains three comma",,,", use this as delimeter
string[] delimitedProperties = text.Split(",,,", StringSplitOptions.RemoveEmptyEntries);
// Comma delimiting
delimitedKeyValuePairs = TextParsingExtensions.CommaDelimitedExpression.Split(text);
}

if (delimitedProperties?.Any() == true)
if (delimitedKeyValuePairs?.Any() == true)
{
foreach (string pair in delimitedKeyValuePairs)
{
foreach (string property in delimitedProperties)
// Note that the current logic accounts for key/value pairs with values that
// contain equal (=) signs. The following are examples of where this is needed.
//
// - Certificate Issuer Distinguished Names (e.g. CN=ABC Infra CA 01, DC=ABC, DC=COM)
// - Certificate Subject Distinguished Names (e.g. CN=any.service.azure.com).

int indexOfEqual = pair.IndexOf("=", StringComparison.InvariantCultureIgnoreCase);
if (indexOfEqual >= 1)
{
if (property.Contains("=", StringComparison.InvariantCultureIgnoreCase))
{
string key = property.Substring(0, property.IndexOf("=", StringComparison.Ordinal));
string value = property.Substring(property.IndexOf("=", StringComparison.Ordinal) + 1);
delimitedValues[key.Trim()] = value.Trim();
}
string key = pair.Substring(0, indexOfEqual)?.Trim();
string value = pair.Substring(key.Length + 1)?.Trim();
delimitedValues[key] = value;
}
}
}
else
{
string[] segments = text.Split('=', StringSplitOptions.TrimEntries);
// Only start at second segment and end at second to last segment
// Because first segment is the key for first pair, and last segment is the value for last pair.
string key = segments[0];
for (int i = 1; i < segments.Length - 1; i++)
{
// This is just to
int lastCommaIndex = segments[i].LastIndexOf(",,,");
int lastSemicolonIndex = segments[i].LastIndexOf(';');
int splitIndex = Math.Max(lastCommaIndex, lastSemicolonIndex);

string value = segments[i].Substring(0, splitIndex);
delimitedValues.Add(key, value);
key = segments[i].Substring(splitIndex).Trim(';').Trim(',');
}

delimitedValues.Add(key, segments[segments.Length - 1]);
}

return delimitedValues;
}
Expand Down
50 changes: 50 additions & 0 deletions src/VirtualClient/VirtualClient.Core.UnitTests/BlobManagerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,51 @@ public void BlobManagerConstructorsValidateRequiredParameters()
Assert.Throws<ArgumentException>(() => new BlobManager(null));
}

[Test]
[TestCase("https://blob.core.windows.net", "https://blob.core.windows.net/packages")]
[TestCase("https://blob.core.windows.net/", "https://blob.core.windows.net/packages")]
[TestCase("https://blob.core.windows.net/packages", "https://blob.core.windows.net/packages")]
[TestCase("https://blob.core.windows.net/packages/", "https://blob.core.windows.net/packages/")]
public void BlobManagerCreatesTheExpectedContainerClientForStorageAccountUris(string uri, string expectedUri)
{
DependencyDescriptor packageDescriptor = new DependencyDescriptor(new Dictionary<string, IConvertible>
{
{ "BlobName", "anypackage.1.0.0.zip" },
{ "ContainerName", "packages" },
{ "Name", "anypackage" }
});

DependencyBlobStore store = new DependencyBlobStore("Packages", uri);
BlobContainerClient client = this.blobManager.CreateContainerClient(new BlobDescriptor(packageDescriptor), store);

Assert.AreEqual(expectedUri, client.Uri.AbsoluteUri);
}

[Test]
[TestCase(
"https://blob.core.windows.net/?sv=2022-11-02&ss=b&srt=co&sp=rwdacytfx&se=2024-06-13T02:35:01Z&st=2024-06-12T18:35:01Z&spr=https",
"https://blob.core.windows.net/packages?sv=2022-11-02&ss=b&srt=co&sp=rwdacytfx&se=2024-06-13T02:35:01Z&st=2024-06-12T18:35:01Z&spr=https")]
[TestCase(
"https://blob.core.windows.net/packages?sv=2022-11-02&ss=b&srt=co&sp=rwdacytfx&se=2024-06-13T02:35:01Z&st=2024-06-12T18:35:01Z&spr=https",
"https://blob.core.windows.net/packages?sv=2022-11-02&ss=b&srt=co&sp=rwdacytfx&se=2024-06-13T02:35:01Z&st=2024-06-12T18:35:01Z&spr=https")]
[TestCase(
"https://blob.core.windows.net/packages/?sv=2022-11-02&ss=b&srt=co&sp=rwdacytfx&se=2024-06-13T02:35:01Z&st=2024-06-12T18:35:01Z&spr=https",
"https://blob.core.windows.net/packages/?sv=2022-11-02&ss=b&srt=co&sp=rwdacytfx&se=2024-06-13T02:35:01Z&st=2024-06-12T18:35:01Z&spr=https")]
public void BlobManagerCreatesTheExpectedContainerClientForStorageAccountSasUris(string uri, string expectedUri)
{
DependencyDescriptor packageDescriptor = new DependencyDescriptor(new Dictionary<string, IConvertible>
{
{ "BlobName", "anypackage.1.0.0.zip" },
{ "ContainerName", "packages" },
{ "Name", "anypackage" }
});

DependencyBlobStore store = new DependencyBlobStore("Packages", uri);
BlobContainerClient client = this.blobManager.CreateContainerClient(new BlobDescriptor(packageDescriptor), store);

Assert.AreEqual(expectedUri, client.Uri.AbsoluteUri);
}

[Test]
public void BlobManagerValidatesTheBlobNameBeforeDownloadingABlob()
{
Expand Down Expand Up @@ -547,6 +592,11 @@ public TestBlobManager(DependencyBlobStore storeDescription)

public Func<BlobDescriptor, Stream, BlobUploadOptions, Response<BlobContentInfo>> OnUploadFromStreamAsync { get; set; }

public new BlobContainerClient CreateContainerClient(BlobDescriptor descriptor, DependencyBlobStore blobStore)
{
return base.CreateContainerClient(descriptor, blobStore);
}

protected override Task<Response> DownloadToStreamAsync(BlobDescriptor descriptor, Stream stream, CancellationToken cancellationToken)
{
Response response = this.OnDownloadToStreamAsync != null
Expand Down
Loading

0 comments on commit fbaf8ff

Please sign in to comment.