Skip to content

Commit

Permalink
Add credential support to PCS API client (dotnet#3610)
Browse files Browse the repository at this point in the history
Co-authored-by: Djuradj Kurepa <[email protected]>
  • Loading branch information
premun and dkurepa authored Jun 10, 2024
1 parent 198d73f commit 90485f2
Show file tree
Hide file tree
Showing 6 changed files with 171 additions and 11 deletions.
4 changes: 2 additions & 2 deletions src/Maestro/SubscriptionActorService/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,8 @@ public static void Configure(IServiceCollection services)
var token = config.GetValue<string>("ProductConstructionService:Token");
return string.IsNullOrEmpty(token)
? ApiFactory.GetAnonymous(uri)
: ApiFactory.GetAuthenticated(uri, token);
? PcsApiFactory.GetAnonymous(uri)
: PcsApiFactory.GetAuthenticated(uri, token);
});

services.AddMergePolicies();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Azure.Core;
using Azure.Identity;

namespace ProductConstructionService.Client
{
/// <summary>
/// A credential that first tries a user-based browser auth flow then falls back to a managed identity-based flow.
/// </summary>
internal class PcsApiCredential : TokenCredential
{
private const string TENANT_ID = "72f988bf-86f1-41af-91ab-2d7cd011db47";

private static readonly Dictionary<string, string> EntraAppIds = new Dictionary<string, string>
{
[ProductConstructionServiceApi.StagingPcsBaseUri.TrimEnd('/')] = "baf98f1b-374e-487d-af42-aa33807f11e4",
};

private readonly TokenRequestContext _requestContext;
private readonly TokenCredential _tokenCredential;

private PcsApiCredential(TokenCredential credential, TokenRequestContext requestContext)
{
_requestContext = requestContext;
_tokenCredential = credential;
}

public override AccessToken GetToken(TokenRequestContext _, CancellationToken cancellationToken)
{
// We hardcode the request context as we know which scopes we need to invoke in each scenario (user vs daemon)
return _tokenCredential.GetToken(_requestContext, cancellationToken);
}

public override ValueTask<AccessToken> GetTokenAsync(TokenRequestContext _, CancellationToken cancellationToken)
{
// We hardcode the request context as we know which scopes we need to invoke in each scenario (user vs daemon)
return _tokenCredential.GetTokenAsync(_requestContext, cancellationToken);
}

/// <summary>
/// Use this for darc invocations from services using an MI
/// </summary>
internal static PcsApiCredential CreateManagedIdentityCredential(string barApiBaseUri, string managedIdentityId)
{
string appId = EntraAppIds[barApiBaseUri.TrimEnd('/')];

var miCredential = new ManagedIdentityCredential(managedIdentityId);

var appCredential = new ClientAssertionCredential(
TENANT_ID,
appId,
async (ct) => (await miCredential.GetTokenAsync(new TokenRequestContext(new string[] { "api://AzureADTokenExchange" }), ct)).Token);

var requestContext = new TokenRequestContext(new string[] { $"api://{appId}/.default" });
return new PcsApiCredential(appCredential, requestContext);
}

/// <summary>
/// Use this for darc invocations from pipelines without a token.
/// </summary>
internal static PcsApiCredential CreateNonUserCredential(string barApiBaseUri)
{
var requestContext = new TokenRequestContext(new string[] { $"{EntraAppIds[barApiBaseUri.TrimEnd('/')]}/.default" });
var credential = new AzureCliCredential();
return new PcsApiCredential(credential, requestContext);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,24 @@

using System;

#nullable enable
namespace ProductConstructionService.Client
{
public static class ApiFactory
public static class PcsApiFactory
{
/// <summary>
/// Obtains API client for authenticated access to internal queues.
/// The client will access production ProductConstructionService instance.
/// </summary>
/// <param name="accessToken">
/// You can get the access token by logging in to your ProductConstructionService instance
/// and proceeding to Profile page.
/// </param>
public static IProductConstructionServiceApi GetAuthenticated(string accessToken)
/// <param name="accessToken">Optional BAR token. When provided, will be used as the primary auth method.</param>
/// <param name="managedIdentityId">Managed Identity to use for the auth</param>
public static IProductConstructionServiceApi GetAuthenticated(
string? accessToken,
string? managedIdentityId)
{
return new ProductConstructionServiceApi(new ProductConstructionServiceApiOptions(new PcsApiTokenCredential(accessToken)));
return new ProductConstructionServiceApi(new ProductConstructionServiceApiOptions(
accessToken,
managedIdentityId));
}

/// <summary>
Expand All @@ -41,9 +44,14 @@ public static IProductConstructionServiceApi GetAnonymous()
/// You can get the access token by logging in to your ProductConstructionService instance
/// and proceeding to Profile page.
/// </param>
public static IProductConstructionServiceApi GetAuthenticated(string baseUri, string accessToken)
public static IProductConstructionServiceApi GetAuthenticated(
string baseUri,
string? accessToken,
string? managedIdentityId)
{
return new ProductConstructionServiceApi(new ProductConstructionServiceApiOptions(new Uri(baseUri), new PcsApiTokenCredential(accessToken)));
return new ProductConstructionServiceApi(new ProductConstructionServiceApiOptions(
accessToken,
managedIdentityId));
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

<ItemGroup>
<PackageReference Include="Azure.Core" />
<PackageReference Include="Azure.Identity" />
<PackageReference Include="Newtonsoft.Json" />
<PackageReference Include="Microsoft.DotNet.SwaggerGenerator.MSBuild" />
</ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Azure.Core;

namespace ProductConstructionService.Client
{
public partial class ProductConstructionServiceApi
{
public const string StagingPcsBaseUri = "https://product-construction-int.delightfuldune-c0f01ab0.westus2.azurecontainerapps.io/";

/// <summary>
/// Creates a credential based on parameters provided.
/// </summary>
/// <param name="barApiBaseUri">BAR API URI used to determine the right set of credentials (INT vs PROD)</param>
/// <param name="barApiToken">Token to use for the call. If none supplied, will try other flows.</param>
/// <param name="managedIdentityId">Managed Identity to use for the auth</param>
/// <returns>Credential that can be used to call the Maestro API</returns>
public static TokenCredential CreateApiCredential(
string barApiBaseUri,
string barApiToken = null,
string managedIdentityId = null)
{
// 1. BAR or Entra token that can directly be used to authenticate against Maestro
if (!string.IsNullOrEmpty(barApiToken))
{
return new PcsApiTokenCredential(barApiToken!);
}

barApiBaseUri ??= StagingPcsBaseUri;

// 2. Managed identity (for server-to-server scenarios)
if (!string.IsNullOrEmpty(managedIdentityId))
{
return PcsApiCredential.CreateManagedIdentityCredential(barApiBaseUri, managedIdentityId!);
}

// 3. Azure CLI authentication (for CI scenarios)
return PcsApiCredential.CreateNonUserCredential(barApiBaseUri);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;

namespace ProductConstructionService.Client
{
public partial class ProductConstructionServiceApiOptions
{
/// <summary>
/// Creates a new instance of <see cref="ProductConstructionServiceApiOptions"/> with the provided base URI.
/// </summary>
/// <param name="baseUri">API base URI</param>
/// <param name="accessToken">Optional BAR token. When provided, will be used as the primary auth method.</param>
/// <param name="managedIdentityId">Managed Identity to use for the auth</param>
public ProductConstructionServiceApiOptions(string baseUri, string accessToken, string managedIdentityId)
: this(
new Uri(baseUri),
ProductConstructionServiceApi.CreateApiCredential(baseUri, accessToken, managedIdentityId))
{
}

/// <summary>
/// Creates a new instance of <see cref="ProductConstructionServiceApiOptions"/> with the provided base URI.
/// </summary>
/// <param name="baseUri">API base URI</param>
/// <param name="accessToken">Optional BAR token. When provided, will be used as the primary auth method.</param>
/// <param name="managedIdentityId">Managed Identity to use for the auth</param>
public ProductConstructionServiceApiOptions(string accessToken, string managedIdentityId)
: this(
new Uri(ProductConstructionServiceApi.StagingPcsBaseUri),
ProductConstructionServiceApi.CreateApiCredential(ProductConstructionServiceApi.StagingPcsBaseUri, accessToken, managedIdentityId))
{
}
}
}

0 comments on commit 90485f2

Please sign in to comment.