Skip to content

Commit ec5cc56

Browse files
authored
Successfully don't add SAS token on blob storage URLs with no permissions (#816)
1 parent ee48a9b commit ec5cc56

File tree

3 files changed

+71
-10
lines changed

3 files changed

+71
-10
lines changed

src/Tes.Runner.Test/Storage/ArmUrlTransformationStrategyTests.cs

+32-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
using Azure.Storage.Blobs.Models;
66
using Azure.Storage.Sas;
77
using CommonUtilities;
8-
using CommonUtilities.Options;
98
using Moq;
109
using Tes.Runner.Models;
1110
using Tes.Runner.Storage;
@@ -16,6 +15,7 @@ namespace Tes.Runner.Test.Storage
1615
public class ArmUrlTransformationStrategyTests
1716
{
1817
private Mock<BlobServiceClient> mockBlobServiceClient = null!;
18+
private Mock<Runner.Transfer.BlobApiHttpUtils> mockBlobApiHttpUtils = null!;
1919
private ArmUrlTransformationStrategy armUrlTransformationStrategy = null!;
2020
private UserDelegationKey userDelegationKey = null!;
2121
const string StorageAccountName = "foo";
@@ -30,12 +30,42 @@ public void SetUp()
3030
AzureEnvironmentConfig = AzureEnvironmentConfig.FromArmEnvironmentEndpoints(CommonUtilities.AzureCloud.AzureCloudConfig.FromKnownCloudNameAsync().Result)
3131
};
3232

33-
armUrlTransformationStrategy = new ArmUrlTransformationStrategy(_ => mockBlobServiceClient.Object, options);
33+
mockBlobApiHttpUtils = new();
34+
mockBlobApiHttpUtils.Setup(x => x.IsEndPointPublic(It.IsAny<Uri>()))
35+
.ReturnsAsync(false);
36+
37+
armUrlTransformationStrategy = new ArmUrlTransformationStrategy(_ => mockBlobServiceClient.Object, options, mockBlobApiHttpUtils.Object);
3438
userDelegationKey = BlobsModelFactory.UserDelegationKey(Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), DateTimeOffset.UtcNow,
3539
DateTimeOffset.UtcNow.AddHours(1), "SIGNED_SERVICE", "V1_0", RunnerTestUtils.GenerateRandomTestAzureStorageKey());
3640
mockBlobServiceClient.Setup(c => c.GetUserDelegationKeyAsync(It.IsAny<DateTimeOffset?>(), It.IsAny<DateTimeOffset>(), It.IsAny<CancellationToken>()))
3741
.ReturnsAsync(Azure.Response.FromValue(userDelegationKey, null!));
42+
}
43+
44+
[TestMethod]
45+
public async Task TransformUrlWithStrategyAsync_BlobStorageUrlWithOutPermissions_WhenRequestingRead_UrlIsReturnAsIs()
46+
{
47+
mockBlobApiHttpUtils.Setup(x => x.IsEndPointPublic(It.IsAny<Uri>()))
48+
.ReturnsAsync(true);
49+
50+
var sourceUrl = $"https://{StorageAccountName}.blob.core.windows.net/cont/blob";
51+
var transformedUrl = await armUrlTransformationStrategy.TransformUrlWithStrategyAsync(sourceUrl, BlobSasPermissions.Read);
52+
53+
Assert.AreEqual(1, mockBlobApiHttpUtils.Invocations.Count);
54+
Assert.IsNotNull(transformedUrl);
55+
var blobUri = new Uri(sourceUrl);
56+
Assert.AreEqual(blobUri.AbsoluteUri, transformedUrl.AbsoluteUri);
57+
}
58+
59+
[TestMethod]
60+
public async Task TransformUrlWithStrategyAsync_BlobStorageUrlWithOutPermissions_WhenRequestingWrite_IsEndPointPublicIsNotCalled()
61+
{
62+
mockBlobApiHttpUtils.Setup(x => x.IsEndPointPublic(It.IsAny<Uri>()))
63+
.ReturnsAsync(true);
64+
65+
var sourceUrl = $"https://{StorageAccountName}.blob.core.windows.net/cont/blob";
66+
var transformedUrl = await armUrlTransformationStrategy.TransformUrlWithStrategyAsync(sourceUrl, BlobSasPermissions.Create);
3867

68+
Assert.IsFalse(mockBlobApiHttpUtils.Invocations.Any());
3969
}
4070

4171
[TestMethod]

src/Tes.Runner/Storage/ArmUrlTransformationStrategy.cs

+15-8
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33

4+
using System.Net;
45
using Azure.Storage.Blobs;
56
using Azure.Storage.Blobs.Models;
67
using Azure.Storage.Sas;
@@ -15,22 +16,25 @@ public class ArmUrlTransformationStrategy : IUrlTransformationStrategy
1516
const string BlobUrlPrefix = ".blob."; // "core.windows.net";
1617
private const int BlobSasTokenExpirationInHours = 24 * 7; //7 days which is the Azure Batch node runtime;
1718
const int UserDelegationKeyExpirationInHours = 1;
19+
private static readonly Lazy<BlobApiHttpUtils> blobApiHttpUtilsFactory = new(() => new());
1820

1921
private readonly ILogger logger = PipelineLoggerFactory.Create<ArmUrlTransformationStrategy>();
2022
private readonly Dictionary<string, UserDelegationKey> userDelegationKeyDictionary = [];
2123
private readonly SemaphoreSlim semaphoreSlim = new(1, 1);
2224
private readonly Func<Uri, BlobServiceClient> blobServiceClientFactory;
2325
private readonly RuntimeOptions runtimeOptions;
2426
private readonly string storageHostSuffix;
27+
private readonly BlobApiHttpUtils blobApiHttpUtils;
2528

26-
public ArmUrlTransformationStrategy(Func<Uri, BlobServiceClient> blobServiceClientFactory, RuntimeOptions runtimeOptions)
29+
public ArmUrlTransformationStrategy(Func<Uri, BlobServiceClient> blobServiceClientFactory, RuntimeOptions runtimeOptions, BlobApiHttpUtils? blobApiHttpUtils = default)
2730
{
2831
ArgumentNullException.ThrowIfNull(blobServiceClientFactory);
2932
ArgumentNullException.ThrowIfNull(runtimeOptions);
3033

3134
this.blobServiceClientFactory = blobServiceClientFactory;
3235
this.runtimeOptions = runtimeOptions;
3336
storageHostSuffix = BlobUrlPrefix + this.runtimeOptions!.AzureEnvironmentConfig!.StorageUrlSuffix;
37+
this.blobApiHttpUtils = blobApiHttpUtils ?? blobApiHttpUtilsFactory.Value;
3438
}
3539

3640
public async Task<Uri> TransformUrlWithStrategyAsync(string sourceUrl, BlobSasPermissions blobSasPermissions)
@@ -60,9 +64,16 @@ private async Task<Uri> GetStorageUriWithSasTokenAsync(string sourceUrl, BlobSas
6064
{
6165
try
6266
{
63-
var blobUrl = new BlobUriBuilder(new Uri(sourceUrl));
67+
Uri uri = new(sourceUrl);
68+
var blobUrl = new BlobUriBuilder(uri);
6469
var blobServiceClient = blobServiceClientFactory(new Uri($"https://{blobUrl.Host}"));
6570

71+
if ((permissions & ~(BlobSasPermissions.Read | BlobSasPermissions.List | BlobSasPermissions.Execute)) == 0 && await blobApiHttpUtils.IsEndPointPublic(uri))
72+
{
73+
logger.LogWarning("The URL provided is not a storage account the managed identity can access. The resolution strategy won't be applied. Host: {Host}", blobUrl.Host);
74+
return uri;
75+
}
76+
6677
var userKey = await GetUserDelegationKeyAsync(blobServiceClient, blobUrl.AccountName);
6778

6879
var sasBuilder = new BlobSasBuilder()
@@ -73,13 +84,9 @@ private async Task<Uri> GetStorageUriWithSasTokenAsync(string sourceUrl, BlobSas
7384
};
7485

7586
sasBuilder.SetPermissions(permissions);
87+
blobUrl.Sas = sasBuilder.ToSasQueryParameters(userKey, blobUrl.AccountName);
7688

77-
var blobUriWithSas = new BlobUriBuilder(blobUrl.ToUri())
78-
{
79-
Sas = sasBuilder.ToSasQueryParameters(userKey, blobUrl.AccountName)
80-
};
81-
82-
return blobUriWithSas.ToUri();
89+
return blobUrl.ToUri();
8390
}
8491
catch (Exception e)
8592
{

src/Tes.Runner/Transfer/BlobApiHttpUtils.cs

+24
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ public static Uri ParsePutBlockUrl(Uri? baseUri, int ordinal)
9393
{
9494
return new Uri($"{baseUri?.AbsoluteUri}&comp=block&blockid={ToBlockId(ordinal)}");
9595
}
96+
9697
public static Uri ParsePutAppendBlockUrl(Uri? baseUri)
9798
{
9899
ArgumentNullException.ThrowIfNull(baseUri);
@@ -180,6 +181,29 @@ public static bool UrlContainsSasToken(string sourceUrl)
180181

181182
return !string.IsNullOrWhiteSpace(blobBuilder.Sas?.Signature);
182183
}
184+
185+
public virtual async Task<bool> IsEndPointPublic(Uri uri)
186+
{
187+
try
188+
{
189+
using var _ = await ExecuteHttpRequestAsync(() => new HttpRequestMessage(HttpMethod.Head, uri));
190+
return true;
191+
192+
}
193+
catch (HttpRequestException e) when (e.StatusCode switch
194+
{
195+
HttpStatusCode.Unauthorized => true,
196+
// Because some servers confuse the difference between 401 & 403
197+
HttpStatusCode.Forbidden => true,
198+
// Azure Blob Storage returns 409 instead of 401
199+
HttpStatusCode.Conflict => true,
200+
_ => false,
201+
})
202+
{
203+
return false;
204+
}
205+
}
206+
183207
private async Task<HttpResponseMessage> ExecuteHttpRequestImplAsync(Func<HttpRequestMessage> request, CancellationToken cancellationToken)
184208
{
185209
HttpResponseMessage? response = null;

0 commit comments

Comments
 (0)