Skip to content

Commit 317378a

Browse files
authored
Support service principal (#10043)
Support service principal
1 parent f334909 commit 317378a

1 file changed

Lines changed: 46 additions & 0 deletions

File tree

src/NuGetGallery.Core/Services/CloudBlobClientWrapper.cs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
5+
using System.Linq;
6+
using System.Security.Cryptography.X509Certificates;
57
using Azure;
68
using Azure.Core;
79
using Azure.Identity;
@@ -57,6 +59,19 @@ public static CloudBlobClientWrapper UsingMsi(
5759
return new CloudBlobClientWrapper(storageConnectionString, tokenCredential, readAccessGeoRedundant, requestTimeout);
5860
}
5961

62+
public static CloudBlobClientWrapper UsingServicePrincipal(
63+
string storageConnectionString,
64+
string appID,
65+
string subjectAlternativeName,
66+
string tenantId,
67+
string authorityHost,
68+
bool readAccessGeoRedundant = false,
69+
TimeSpan? requestTimeout = null)
70+
{
71+
var tokenCredential = GetCredentialUsingServicePrincipal(appID, subjectAlternativeName, tenantId, authorityHost);
72+
return new CloudBlobClientWrapper(storageConnectionString, tokenCredential, readAccessGeoRedundant, requestTimeout);
73+
}
74+
6075
public ISimpleCloudBlob GetBlobFromUri(Uri uri)
6176
{
6277
// For Azure blobs, the query string is assumed to be the SAS token.
@@ -217,5 +232,36 @@ private string GetSecondaryConnectionString()
217232
.Replace($"AccountName={primaryAccountName};", $"AccountName={secondaryAccountName};");
218233
return secondaryConnectionString;
219234
}
235+
236+
/// <summary>
237+
/// Gets credential using the Service Principal. If the resource is in a different tenant, this is how to access it.
238+
/// The ServicePrincipal needs to be a "Storage Table/Blob/Queue Data Contributor" role on the storage account. Owner isn't enough.
239+
/// </summary>
240+
/// <returns>ClientCertificatCredential to be used to communicate with Storage.</returns>
241+
private static ClientCertificateCredential GetCredentialUsingServicePrincipal(string appID, string subjectAlternativeName, string tenantId, string authorityHost)
242+
{
243+
X509Certificate2 clientCert;
244+
245+
// Azure.Identity library doesn't support referencing cert by Store + Subject name, so we need to load it ourselves.
246+
using (X509Store store = new X509Store(StoreName.My, StoreLocation.CurrentUser))
247+
{
248+
store.Open(OpenFlags.ReadOnly);
249+
250+
X509Certificate2Collection certs = store.Certificates.Find(X509FindType.FindBySubjectName, subjectAlternativeName, true);
251+
252+
if (certs.Count == 0)
253+
{
254+
throw new InvalidOperationException($"Unable to find certificate with subject name '{subjectAlternativeName}'");
255+
}
256+
257+
// As an exception to comment in GetKeyVaultCertsAsync method, this X509Certificate2 object does not have to be disposed
258+
// because it is referencing a platform certificate from CurrentUser certificate store, so no temporary files are created for this object.
259+
clientCert = certs.Cast<X509Certificate2>()
260+
.Where(c => c.NotBefore < DateTime.UtcNow && c.NotAfter > DateTime.UtcNow)
261+
.OrderBy(x => x.NotAfter).Last();
262+
}
263+
264+
return new ClientCertificateCredential(tenantId, appID, clientCert, new ClientCertificateCredentialOptions { AuthorityHost = new Uri(authorityHost), SendCertificateChain = true });
265+
}
220266
}
221267
}

0 commit comments

Comments
 (0)