Skip to content

Commit f074249

Browse files
authored
Merge pull request #242 from thucnguyen77/cosmos-extension/client-secret-credential-support
Cosmos extension/client secret credential support
2 parents b841801 + 6d2af70 commit f074249

8 files changed

Lines changed: 904 additions & 37 deletions

File tree

Extensions/Cosmos/Cosmos.DataTransfer.CosmosExtension.UnitTests/Cosmos.DataTransfer.CosmosExtension.UnitTests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
<ItemGroup>
1515
<PackageReference Include="Microsoft.Extensions.Configuration" />
1616
<PackageReference Include="Microsoft.NET.Test.Sdk" />
17+
<PackageReference Include="Moq" />
1718
<PackageReference Include="MSTest.TestAdapter" />
1819
<PackageReference Include="MSTest.TestFramework" />
1920
<PackageReference Include="Newtonsoft.Json" />
Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
using Azure.Identity;
2+
using Microsoft.Extensions.Logging;
3+
using Moq;
4+
using System.Security.Cryptography;
5+
using System.Security.Cryptography.X509Certificates;
6+
7+
namespace Cosmos.DataTransfer.CosmosExtension.UnitTests;
8+
9+
[TestClass]
10+
public class CosmosExtensionServicesCredentialTests
11+
{
12+
[TestMethod]
13+
public void GetTokenCredentialSelection_WithNoServicePrincipalInfo_ReturnsDefaultCredential()
14+
{
15+
var settings = new CosmosSourceSettings
16+
{
17+
UseRbacAuth = true,
18+
AccountEndpoint = "https://localhost:8081/",
19+
Database = "db",
20+
Container = "container",
21+
};
22+
23+
var selection = CosmosExtensionServices.GetTokenCredentialSelection(settings);
24+
25+
Assert.AreEqual(CosmosExtensionServices.TokenCredentialSelection.DefaultAzureCredential, selection);
26+
}
27+
28+
[TestMethod]
29+
public void GetTokenCredentialSelection_WithTenantClientAndSecret_ReturnsClientSecretCredential()
30+
{
31+
var settings = new CosmosSourceSettings
32+
{
33+
UseRbacAuth = true,
34+
AccountEndpoint = "https://localhost:8081/",
35+
Database = "db",
36+
Container = "container",
37+
TenantId = "tenant-id",
38+
ClientId = "client-id",
39+
ClientSecret = "client-secret",
40+
};
41+
42+
var selection = CosmosExtensionServices.GetTokenCredentialSelection(settings);
43+
44+
Assert.AreEqual(CosmosExtensionServices.TokenCredentialSelection.ClientSecretCredential, selection);
45+
}
46+
47+
[TestMethod]
48+
public void GetTokenCredentialSelection_WithWhitespaceServicePrincipalInfo_ReturnsDefaultCredential()
49+
{
50+
var settings = new CosmosSourceSettings
51+
{
52+
UseRbacAuth = true,
53+
AccountEndpoint = "https://localhost:8081/",
54+
Database = "db",
55+
Container = "container",
56+
TenantId = " ",
57+
ClientId = " ",
58+
ClientSecret = "client-secret",
59+
};
60+
61+
var selection = CosmosExtensionServices.GetTokenCredentialSelection(settings);
62+
63+
Assert.AreEqual(CosmosExtensionServices.TokenCredentialSelection.DefaultAzureCredential, selection);
64+
}
65+
66+
[TestMethod]
67+
public void GetTokenCredentialSelection_WithTenantClientAndCertificatePath_ReturnsClientCertificateCredential()
68+
{
69+
var settings = new CosmosSourceSettings
70+
{
71+
UseRbacAuth = true,
72+
AccountEndpoint = "https://localhost:8081/",
73+
Database = "db",
74+
Container = "container",
75+
TenantId = "tenant-id",
76+
ClientId = "client-id",
77+
ClientCertificatePath = "./certs/cert.pfx",
78+
};
79+
80+
var selection = CosmosExtensionServices.GetTokenCredentialSelection(settings);
81+
82+
Assert.AreEqual(CosmosExtensionServices.TokenCredentialSelection.ClientCertificateCredential, selection);
83+
}
84+
85+
[TestMethod]
86+
public void CreateRbacTokenCredential_WithNoServicePrincipalInfo_ReturnsDefaultAzureCredential()
87+
{
88+
var loggerMock = new Mock<ILogger>();
89+
var settings = new CosmosSourceSettings
90+
{
91+
UseRbacAuth = true,
92+
AccountEndpoint = "https://localhost:8081/",
93+
Database = "db",
94+
Container = "container",
95+
};
96+
97+
var credential = CosmosExtensionServices.CreateRbacTokenCredential(settings, loggerMock.Object);
98+
99+
Assert.IsInstanceOfType<DefaultAzureCredential>(credential);
100+
}
101+
102+
[TestMethod]
103+
public void CreateRbacTokenCredential_WithInvalidCertificatePath_ThrowsFriendlyConfigurationError()
104+
{
105+
var loggerMock = new Mock<ILogger>();
106+
var settings = new CosmosSourceSettings
107+
{
108+
UseRbacAuth = true,
109+
AccountEndpoint = "https://localhost:8081/",
110+
Database = "db",
111+
Container = "container",
112+
TenantId = "tenant-id",
113+
ClientId = "client-id",
114+
ClientCertificatePath = "./certs/does-not-exist.pfx",
115+
};
116+
117+
var ex = Assert.ThrowsException<InvalidOperationException>(() =>
118+
CosmosExtensionServices.CreateRbacTokenCredential(settings, loggerMock.Object));
119+
120+
StringAssert.Contains(ex.Message, "Failed to configure RBAC credentials");
121+
Assert.IsNotNull(ex.InnerException);
122+
}
123+
124+
[TestMethod]
125+
public void CreateRbacTokenCredential_WithPasswordProtectedCertificate_ReturnsClientCertificateCredential()
126+
{
127+
var loggerMock = new Mock<ILogger>();
128+
const string certPassword = "test-password";
129+
var certPath = CreatePasswordProtectedPfx(certPassword);
130+
131+
try
132+
{
133+
var settings = new CosmosSourceSettings
134+
{
135+
UseRbacAuth = true,
136+
AccountEndpoint = "https://localhost:8081/",
137+
Database = "db",
138+
Container = "container",
139+
TenantId = "tenant-id",
140+
ClientId = "client-id",
141+
ClientCertificatePath = certPath,
142+
ClientCertificatePassword = certPassword,
143+
};
144+
145+
var credential = CosmosExtensionServices.CreateRbacTokenCredential(settings, loggerMock.Object);
146+
147+
Assert.IsInstanceOfType<ClientCertificateCredential>(credential);
148+
loggerMock.Verify(
149+
x => x.Log(
150+
LogLevel.Warning,
151+
It.IsAny<EventId>(),
152+
It.Is<It.IsAnyType>((v, t) => v.ToString()!.Contains(nameof(CosmosSourceSettings.ClientCertificatePassword))),
153+
It.IsAny<Exception?>(),
154+
It.IsAny<Func<It.IsAnyType, Exception?, string>>()),
155+
Times.Once);
156+
}
157+
finally
158+
{
159+
if (File.Exists(certPath))
160+
{
161+
File.Delete(certPath);
162+
}
163+
}
164+
}
165+
166+
[TestMethod]
167+
public void CreateRbacTokenCredential_WithCertificateWithoutPrivateKey_ThrowsFriendlyConfigurationError()
168+
{
169+
var loggerMock = new Mock<ILogger>();
170+
var certPath = CreatePublicCertificate();
171+
172+
try
173+
{
174+
var settings = new CosmosSourceSettings
175+
{
176+
UseRbacAuth = true,
177+
AccountEndpoint = "https://localhost:8081/",
178+
Database = "db",
179+
Container = "container",
180+
TenantId = "tenant-id",
181+
ClientId = "client-id",
182+
ClientCertificatePath = certPath,
183+
};
184+
185+
var ex = Assert.ThrowsException<InvalidOperationException>(() =>
186+
CosmosExtensionServices.CreateRbacTokenCredential(settings, loggerMock.Object));
187+
188+
StringAssert.Contains(ex.Message, "Failed to configure RBAC credentials");
189+
Assert.IsInstanceOfType<CryptographicException>(ex.InnerException);
190+
StringAssert.Contains(ex.InnerException!.Message, "private key");
191+
}
192+
finally
193+
{
194+
if (File.Exists(certPath))
195+
{
196+
File.Delete(certPath);
197+
}
198+
}
199+
}
200+
201+
[TestMethod]
202+
public void CreateClientOptions_UsesAllowBulkExecutionSetting()
203+
{
204+
var loggerMock = new Mock<ILogger>();
205+
var settings = new CosmosSourceSettings
206+
{
207+
ConnectionString = "AccountEndpoint=https://localhost:8081/;AccountKey=key",
208+
Database = "db",
209+
Container = "container",
210+
AllowBulkExecution = true,
211+
};
212+
213+
var clientOptions = CosmosExtensionServices.CreateClientOptions(settings, "test-agent", loggerMock.Object);
214+
215+
Assert.IsTrue(clientOptions.AllowBulkExecution);
216+
217+
settings.AllowBulkExecution = false;
218+
clientOptions = CosmosExtensionServices.CreateClientOptions(settings, "test-agent", loggerMock.Object);
219+
220+
Assert.IsFalse(clientOptions.AllowBulkExecution);
221+
}
222+
223+
private static string CreatePasswordProtectedPfx(string password)
224+
{
225+
using var rsa = RSA.Create(2048);
226+
var request = new CertificateRequest("CN=unit-test-cert", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
227+
using var certificate = request.CreateSelfSigned(DateTimeOffset.UtcNow.AddDays(-1), DateTimeOffset.UtcNow.AddDays(1));
228+
var pfxBytes = certificate.Export(X509ContentType.Pfx, password);
229+
var certPath = Path.Combine(Path.GetTempPath(), $"dmt-test-{Guid.NewGuid():N}.pfx");
230+
File.WriteAllBytes(certPath, pfxBytes);
231+
return certPath;
232+
}
233+
234+
private static string CreatePublicCertificate()
235+
{
236+
using var rsa = RSA.Create(2048);
237+
var request = new CertificateRequest("CN=unit-test-cert", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
238+
using var certificate = request.CreateSelfSigned(DateTimeOffset.UtcNow.AddDays(-1), DateTimeOffset.UtcNow.AddDays(1));
239+
var certBytes = certificate.Export(X509ContentType.Cert);
240+
var certPath = Path.Combine(Path.GetTempPath(), $"dmt-test-{Guid.NewGuid():N}.cer");
241+
File.WriteAllBytes(certPath, certBytes);
242+
return certPath;
243+
}
244+
}

0 commit comments

Comments
 (0)