-
Notifications
You must be signed in to change notification settings - Fork 27
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
WX-1594 Support for private ACR access in Terra (#715)
Co-authored-by: Blair L Murri <[email protected]>
- Loading branch information
1 parent
8ed59a8
commit 18fbd38
Showing
28 changed files
with
822 additions
and
79 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
// Copyright (c) Microsoft Corporation. | ||
// Licensed under the MIT License. | ||
|
||
using Azure.Core; | ||
using CommonUtilities; | ||
using Microsoft.Extensions.Logging.Abstractions; | ||
using Moq; | ||
using Tes.ApiClients.Models.Terra; | ||
|
||
namespace Tes.ApiClients.Tests | ||
{ | ||
[TestClass, TestCategory("Unit")] | ||
public class TerraSamApiClientTests | ||
{ | ||
private TerraSamApiClient terraSamApiClient = null!; | ||
private Mock<TokenCredential> tokenCredential = null!; | ||
private Mock<CachingRetryPolicyBuilder> cacheAndRetryBuilder = null!; | ||
private Lazy<Mock<CachingRetryHandler.CachingAsyncRetryHandlerPolicy<HttpResponseMessage>>> cacheAndRetryHandler = null!; | ||
private TerraApiStubData terraApiStubData = null!; | ||
private AzureEnvironmentConfig azureEnvironmentConfig = null!; | ||
private TimeSpan cacheTTL = TimeSpan.FromMinutes(1); | ||
|
||
[TestInitialize] | ||
public void SetUp() | ||
{ | ||
terraApiStubData = new TerraApiStubData(); | ||
tokenCredential = new Mock<TokenCredential>(); | ||
cacheAndRetryBuilder = new Mock<CachingRetryPolicyBuilder>(); | ||
var cache = new Mock<Microsoft.Extensions.Caching.Memory.IMemoryCache>(); | ||
cache.Setup(c => c.CreateEntry(It.IsAny<object>())).Returns(new Mock<Microsoft.Extensions.Caching.Memory.ICacheEntry>().Object); | ||
cacheAndRetryBuilder.SetupGet(c => c.AppCache).Returns(cache.Object); | ||
cacheAndRetryHandler = new(TestServices.RetryHandlersHelpers.GetCachingAsyncRetryPolicyMock(cacheAndRetryBuilder, c => c.DefaultRetryHttpResponseMessagePolicyBuilder())); | ||
azureEnvironmentConfig = ExpensiveObjectTestUtility.AzureCloudConfig.AzureEnvironmentConfig!; | ||
|
||
terraSamApiClient = new TerraSamApiClient(TerraApiStubData.SamApiHost, tokenCredential.Object, | ||
cacheAndRetryBuilder.Object, azureEnvironmentConfig, NullLogger<TerraSamApiClient>.Instance); | ||
} | ||
|
||
[TestMethod] | ||
public async Task GetActionManagedIdentityAsync_ValidRequest_ReturnsPayload() | ||
{ | ||
cacheAndRetryHandler.Value.Setup(c => c.ExecuteWithRetryConversionAndCachingAsync( | ||
It.IsAny<string>(), | ||
It.IsAny<Func<CancellationToken, Task<HttpResponseMessage>>>(), | ||
It.IsAny<Func<HttpResponseMessage, CancellationToken, Task<SamActionManagedIdentityApiResponse>>>(), | ||
It.IsAny<DateTimeOffset>(), | ||
It.IsAny<CancellationToken>(), | ||
It.IsAny<string>())) | ||
.ReturnsAsync(System.Text.Json.JsonSerializer.Deserialize<SamActionManagedIdentityApiResponse>(terraApiStubData.GetSamActionManagedIdentityApiResponseInJson())!); | ||
|
||
var apiResponse = await terraSamApiClient.GetActionManagedIdentityForACRPullAsync(terraApiStubData.AcrPullIdentitySamResourceId, cacheTTL, CancellationToken.None); | ||
|
||
Assert.IsNotNull(apiResponse); | ||
Assert.IsTrue(!string.IsNullOrEmpty(apiResponse.ObjectId)); | ||
Assert.IsTrue(apiResponse.ObjectId.Contains(TerraApiStubData.TerraPetName)); | ||
} | ||
|
||
[TestMethod] | ||
public async Task GetActionManagedIdentityAsync_ValidRequest_Returns404() | ||
{ | ||
cacheAndRetryHandler.Value.Setup(c => c.ExecuteWithRetryConversionAndCachingAsync( | ||
It.IsAny<string>(), | ||
It.IsAny<Func<CancellationToken, Task<HttpResponseMessage>>>(), | ||
It.IsAny<Func<HttpResponseMessage, CancellationToken, Task<SamActionManagedIdentityApiResponse>>>(), | ||
It.IsAny<DateTimeOffset>(), | ||
It.IsAny<CancellationToken>(), | ||
It.IsAny<string>())) | ||
.Throws(new HttpRequestException(null, null, System.Net.HttpStatusCode.NotFound)); | ||
|
||
var apiResponse = await terraSamApiClient.GetActionManagedIdentityForACRPullAsync(terraApiStubData.AcrPullIdentitySamResourceId, cacheTTL, CancellationToken.None); | ||
|
||
Assert.IsNull(apiResponse); | ||
} | ||
|
||
[TestMethod] | ||
public async Task GetActionManagedIdentityAsync_ValidRequest_Returns500() | ||
{ | ||
cacheAndRetryHandler.Value.Setup(c => c.ExecuteWithRetryConversionAndCachingAsync( | ||
It.IsAny<string>(), | ||
It.IsAny<Func<CancellationToken, Task<HttpResponseMessage>>>(), | ||
It.IsAny<Func<HttpResponseMessage, CancellationToken, Task<SamActionManagedIdentityApiResponse>>>(), | ||
It.IsAny<DateTimeOffset>(), | ||
It.IsAny<CancellationToken>(), | ||
It.IsAny<string>())) | ||
.Throws(new HttpRequestException(null, null, System.Net.HttpStatusCode.BadGateway)); | ||
|
||
await Assert.ThrowsExceptionAsync<HttpRequestException>(async () => await terraSamApiClient.GetActionManagedIdentityForACRPullAsync(terraApiStubData.AcrPullIdentitySamResourceId, cacheTTL, CancellationToken.None)); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
61 changes: 61 additions & 0 deletions
61
src/Tes.ApiClients/Models/Terra/SamActionManagedIdentityApiResponse.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
// Copyright (c) Microsoft Corporation. | ||
// Licensed under the MIT License. | ||
|
||
using System.Text.Json.Serialization; | ||
|
||
namespace Tes.ApiClients.Models.Terra | ||
{ | ||
public class SamActionManagedIdentityApiResponse | ||
{ | ||
[JsonPropertyName("id")] | ||
public ActionManagedIdentityId actionManagedIdentityId { get; set; } | ||
|
||
[JsonPropertyName("displayName")] | ||
public string DisplayName { get; set; } | ||
|
||
[JsonPropertyName("managedResourceGroupCoordinates")] | ||
public ManagedResourceGroupCoordinates managedResourceGroupCoordinates { get; set; } | ||
|
||
[JsonPropertyName("objectId")] | ||
public string ObjectId { get; set; } | ||
|
||
} | ||
|
||
public class ActionManagedIdentityId | ||
{ | ||
[JsonPropertyName("resourceId")] | ||
public FullyQualifiedResourceId ResourceId { get; set; } | ||
|
||
[JsonPropertyName("action")] | ||
public string Action { get; set; } | ||
|
||
[JsonPropertyName("billingProfileId")] | ||
public Guid BillingProfileId { get; set; } | ||
} | ||
|
||
public class FullyQualifiedResourceId | ||
{ | ||
[JsonPropertyName("resourceTypeName")] | ||
public string ResourceTypeName { get; set; } | ||
|
||
[JsonPropertyName("resourceId")] | ||
public string ResourceId { get; set; } | ||
} | ||
|
||
public class ManagedResourceGroupCoordinates | ||
{ | ||
[JsonPropertyName("tenantId")] | ||
public Guid TenantId { get; set; } | ||
|
||
[JsonPropertyName("subscriptionId")] | ||
public Guid SubscriptionId { get; set; } | ||
|
||
[JsonPropertyName("managedResourceGroupName")] | ||
public string ManagedResourceGroupName { get; set; } | ||
} | ||
|
||
[JsonSerializable(typeof(SamActionManagedIdentityApiResponse))] | ||
public partial class SamActionManagedIdentityApiResponseContext : JsonSerializerContext | ||
{ } | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
// Copyright (c) Microsoft Corporation. | ||
// Licensed under the MIT License. | ||
|
||
using Azure.Core; | ||
using CommonUtilities; | ||
using Microsoft.Extensions.Caching.Memory; | ||
using Microsoft.Extensions.Logging; | ||
using Tes.ApiClients.Models.Terra; | ||
|
||
namespace Tes.ApiClients | ||
{ | ||
/// <summary> | ||
/// Terra Sam api client | ||
/// Sam manages authorization and IAM functionality | ||
/// </summary> | ||
public class TerraSamApiClient : TerraApiClient | ||
{ | ||
private const string SamApiSegments = @"/api/azure/v1"; | ||
|
||
private static readonly IMemoryCache SharedMemoryCache = new MemoryCache(new MemoryCacheOptions()); | ||
|
||
/// <summary> | ||
/// Constructor of TerraSamApiClient | ||
/// </summary> | ||
/// <param name="apiUrl">Sam API host</param> | ||
/// <param name="tokenCredential"></param> | ||
/// <param name="cachingRetryHandler"></param> | ||
/// <param name="azureCloudIdentityConfig"></param> | ||
/// <param name="logger"></param> | ||
public TerraSamApiClient(string apiUrl, TokenCredential tokenCredential, CachingRetryPolicyBuilder cachingRetryHandler, | ||
AzureEnvironmentConfig azureCloudIdentityConfig, ILogger<TerraSamApiClient> logger) : base(apiUrl, tokenCredential, cachingRetryHandler, azureCloudIdentityConfig, logger) | ||
{ } | ||
|
||
public static TerraSamApiClient CreateTerraSamApiClient(string apiUrl, TokenCredential tokenCredential, AzureEnvironmentConfig azureCloudIdentityConfig) | ||
{ | ||
return CreateTerraApiClient<TerraSamApiClient>(apiUrl, SharedMemoryCache, tokenCredential, azureCloudIdentityConfig); | ||
} | ||
|
||
/// <summary> | ||
/// Protected parameter-less constructor | ||
/// </summary> | ||
protected TerraSamApiClient() { } | ||
|
||
public virtual async Task<SamActionManagedIdentityApiResponse> GetActionManagedIdentityForACRPullAsync(Guid resourceId, TimeSpan cacheTTL, CancellationToken cancellationToken) | ||
{ | ||
return await GetActionManagedIdentityAsync("private_azure_container_registry", resourceId, "pull_image", cacheTTL, cancellationToken); | ||
} | ||
|
||
private async Task<SamActionManagedIdentityApiResponse> GetActionManagedIdentityAsync(string resourceType, Guid resourceId, string action, TimeSpan cacheTTL, CancellationToken cancellationToken) | ||
{ | ||
ArgumentNullException.ThrowIfNull(resourceId); | ||
|
||
var url = GetSamActionManagedIdentityUrl(resourceType, resourceId, action); | ||
|
||
Logger.LogInformation(@"Fetching action managed identity from Sam for {resourceId}", resourceId); | ||
|
||
try | ||
{ | ||
return await HttpGetRequestWithExpirableCachingAndRetryPolicyAsync(url, | ||
SamActionManagedIdentityApiResponseContext.Default.SamActionManagedIdentityApiResponse, cacheTTL, cancellationToken, setAuthorizationHeader: true); | ||
} | ||
catch (HttpRequestException e) | ||
{ | ||
// Sam will return a 404 if there is no action identity that matches the query, | ||
// or if we don't have access to it. | ||
if (e.StatusCode == System.Net.HttpStatusCode.NotFound) | ||
{ | ||
return null; | ||
} | ||
else | ||
{ | ||
throw; | ||
} | ||
|
||
} | ||
} | ||
|
||
public virtual Uri GetSamActionManagedIdentityUrl(string resourceType, Guid resourceId, string action) | ||
{ | ||
var apiRequestUrl = $"{ApiUrl.TrimEnd('/')}{SamApiSegments}/actionManagedIdentity/{resourceType}/{resourceId}/{action}"; | ||
|
||
var uriBuilder = new UriBuilder(apiRequestUrl); | ||
|
||
return uriBuilder.Uri; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.