Skip to content

Commit 46da4bf

Browse files
gzuberfabiocav
authored andcommitted
Added feature to connect to blob secret storage with sas and supporting tests.
1 parent bb5b80e commit 46da4bf

File tree

6 files changed

+118
-29
lines changed

6 files changed

+118
-29
lines changed
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT License. See License.txt in the project root for license information.
3+
4+
using System;
5+
using Microsoft.WindowsAzure.Storage.Blob;
6+
7+
namespace Microsoft.Azure.WebJobs.Script.WebHost
8+
{
9+
/// <summary>
10+
/// An <see cref="BlobStorageSecretsRepository"/> implementation that uses a SAS connection string to connect to Azure blob storage.
11+
/// </summary>
12+
public sealed class BlobStorageSasSecretsRepository : BlobStorageSecretsRepository
13+
{
14+
public BlobStorageSasSecretsRepository(string secretSentinelDirectoryPath, string containerSasUri, string siteSlotName)
15+
: base(secretSentinelDirectoryPath, containerSasUri, siteSlotName)
16+
{ }
17+
18+
protected override CloudBlobContainer CreateBlobContainer(string containerSasUri)
19+
{
20+
return new CloudBlobContainer(new Uri(containerSasUri));
21+
}
22+
}
23+
}

src/WebJobs.Script.WebHost/Security/KeyManagement/BlobStorageSecretsRepository.cs

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ namespace Microsoft.Azure.WebJobs.Script.WebHost
1717
/// <summary>
1818
/// An <see cref="ISecretsRepository"/> implementation that uses Azure blob storage as the backing store.
1919
/// </summary>
20-
public sealed class BlobStorageSecretsRepository : BaseSecretsRepository
20+
public class BlobStorageSecretsRepository : BaseSecretsRepository
2121
{
2222
private readonly string _secretsBlobPath;
2323
private readonly string _hostSecretsBlobPath;
@@ -44,13 +44,7 @@ public BlobStorageSecretsRepository(string secretSentinelDirectoryPath, string a
4444
_hostSecretsBlobPath = string.Format("{0}/{1}", _secretsBlobPath, ScriptConstants.HostMetadataFileName);
4545

4646
_accountConnectionString = accountConnectionString;
47-
CloudStorageAccount account = CloudStorageAccount.Parse(_accountConnectionString);
48-
CloudBlobClient client = account.CreateCloudBlobClient();
49-
50-
_blobContainer = client.GetContainerReference(_secretsContainerName);
51-
52-
// TODO: Remove this (it is already slated to be removed)
53-
_blobContainer.CreateIfNotExistsAsync().GetAwaiter().GetResult();
47+
_blobContainer = CreateBlobContainer(_accountConnectionString);
5448
}
5549

5650
public override bool IsEncryptionSupported
@@ -61,6 +55,18 @@ public override bool IsEncryptionSupported
6155
}
6256
}
6357

58+
protected virtual CloudBlobContainer CreateBlobContainer(string connectionString)
59+
{
60+
CloudStorageAccount account = CloudStorageAccount.Parse(_accountConnectionString);
61+
CloudBlobClient client = account.CreateCloudBlobClient();
62+
CloudBlobContainer container = client.GetContainerReference(_secretsContainerName);
63+
64+
// TODO: Remove this (it is already slated to be removed)
65+
container.CreateIfNotExistsAsync().GetAwaiter().GetResult();
66+
67+
return container;
68+
}
69+
6470
public override async Task<ScriptSecrets> ReadAsync(ScriptSecretsType type, string functionName)
6571
{
6672
string secretsContent = null;

src/WebJobs.Script.WebHost/Security/KeyManagement/DefaultSecretManagerProvider.cs

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ namespace Microsoft.Azure.WebJobs.Script.WebHost
1515
public sealed class DefaultSecretManagerProvider : ISecretManagerProvider
1616
{
1717
private const string FileStorage = "Files";
18+
private const string BlobStorage = "Blob";
1819
private readonly ILogger _logger;
1920
private readonly IMetricsLogger _metricsLogger;
2021
private readonly IOptionsMonitor<ScriptApplicationHostOptions> _options;
@@ -56,6 +57,7 @@ internal ISecretsRepository CreateSecretsRepository()
5657
{
5758
string secretStorageType = Environment.GetEnvironmentVariable(EnvironmentSettingNames.AzureWebJobsSecretStorageType);
5859
string storageString = _configuration.GetWebJobsConnectionString(ConnectionStringNames.Storage);
60+
string secretStorageSas = _environment.GetEnvironmentVariable(EnvironmentSettingNames.AzureWebJobsSecretStorageSas);
5961
if (secretStorageType != null && secretStorageType.Equals(FileStorage, StringComparison.OrdinalIgnoreCase))
6062
{
6163
return new FileSystemSecretsRepository(_options.CurrentValue.SecretsPath);
@@ -70,16 +72,21 @@ internal ISecretsRepository CreateSecretsRepository()
7072
{
7173
return new KubernetesSecretsRepository(_environment, new SimpleKubernetesClient(_environment));
7274
}
73-
else if (storageString == null)
75+
else if (secretStorageSas != null)
7476
{
75-
throw new InvalidOperationException($"Secret initialization from Blob storage failed due to a missing Azure Storage connection string. If you intend to use files for secrets, add an App Setting key '{EnvironmentSettingNames.AzureWebJobsSecretStorageType}' with value '{FileStorage}'.");
77+
string siteSlotName = _environment.GetAzureWebsiteUniqueSlotName() ?? _hostIdProvider.GetHostIdAsync(CancellationToken.None).GetAwaiter().GetResult();
78+
return new BlobStorageSasSecretsRepository(Path.Combine(_options.CurrentValue.SecretsPath, "Sentinels"), secretStorageSas, siteSlotName);
7679
}
77-
else
80+
else if (storageString != null)
7881
{
7982
string siteSlotName = _environment.GetAzureWebsiteUniqueSlotName() ?? _hostIdProvider.GetHostIdAsync(CancellationToken.None).GetAwaiter().GetResult();
80-
8183
return new BlobStorageSecretsRepository(Path.Combine(_options.CurrentValue.SecretsPath, "Sentinels"), storageString, siteSlotName);
8284
}
85+
else
86+
{
87+
throw new InvalidOperationException($"Secret initialization from Blob storage failed due to missing both an Azure Storage connection string and a SAS connection uri. " +
88+
$"For Blob Storage, please provide at least one of these. If you intend to use files for secrets, add an App Setting key '{EnvironmentSettingNames.AzureWebJobsSecretStorageType}' with value '{FileStorage}'.");
89+
}
8390
}
8491
}
8592
}

src/WebJobs.Script/Environment/EnvironmentSettingNames.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ public static class EnvironmentSettingNames
2020
public const string TypeScriptCompilerPath = "AzureWebJobs_TypeScriptPath";
2121
public const string AzureWebsiteAppCountersName = "WEBSITE_COUNTERS_APP";
2222
public const string AzureWebJobsSecretStorageType = "AzureWebJobsSecretStorageType";
23+
public const string AzureWebJobsSecretStorageSas = "AzureWebJobsSecretStorageSas";
2324
public const string AppInsightsInstrumentationKey = "APPINSIGHTS_INSTRUMENTATIONKEY";
2425
public const string AppInsightsQuickPulseAuthApiKey = "APPINSIGHTS_QUICKPULSEAUTHAPIKEY";
2526
public const string FunctionsExtensionVersion = "FUNCTIONS_EXTENSION_VERSION";

test/WebJobs.Script.Tests.Integration/Host/SecretsRepositoryTests.cs

Lines changed: 51 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ public enum SecretsRepositoryType
3636
{
3737
FileSystem,
3838
BlobStorage,
39+
BlobStorageSas,
3940
KeyVault
4041
}
4142

@@ -45,10 +46,12 @@ public async Task FileSystemRepo_Constructor_CreatesSecretPathIfNotExists()
4546
await Constructor_CreatesSecretPathIfNotExists(SecretsRepositoryType.FileSystem);
4647
}
4748

48-
[Fact]
49-
public async Task BlobStorageRepo_Constructor_CreatesSecretPathIfNotExists()
49+
[Theory]
50+
[InlineData(SecretsRepositoryType.BlobStorage)]
51+
[InlineData(SecretsRepositoryType.BlobStorageSas)]
52+
public async Task BlobStorageRepo_Constructor_CreatesSecretPathIfNotExists(SecretsRepositoryType repositoryType)
5053
{
51-
await Constructor_CreatesSecretPathIfNotExists(SecretsRepositoryType.BlobStorage);
54+
await Constructor_CreatesSecretPathIfNotExists(repositoryType);
5255
}
5356

5457
[Fact]
@@ -80,11 +83,13 @@ private async Task Constructor_CreatesSecretPathIfNotExists(SecretsRepositoryTyp
8083
}
8184

8285
[Theory]
83-
[InlineData(ScriptSecretsType.Host)]
84-
[InlineData(ScriptSecretsType.Function)]
85-
public async Task BlobStorageRepo_ReadAsync_ReadsExpectedFile(ScriptSecretsType secretsType)
86+
[InlineData(SecretsRepositoryType.BlobStorage, ScriptSecretsType.Host)]
87+
[InlineData(SecretsRepositoryType.BlobStorage, ScriptSecretsType.Function)]
88+
[InlineData(SecretsRepositoryType.BlobStorageSas, ScriptSecretsType.Host)]
89+
[InlineData(SecretsRepositoryType.BlobStorageSas, ScriptSecretsType.Function)]
90+
public async Task BlobStorageRepo_ReadAsync_ReadsExpectedFile(SecretsRepositoryType repositoryType, ScriptSecretsType secretsType)
8691
{
87-
await ReadAsync_ReadsExpectedFile(SecretsRepositoryType.BlobStorage, secretsType);
92+
await ReadAsync_ReadsExpectedFile(repositoryType, secretsType);
8893
}
8994

9095
[Theory]
@@ -152,11 +157,13 @@ private async Task ReadAsync_ReadsExpectedFile(SecretsRepositoryType repositoryT
152157
}
153158

154159
[Theory]
155-
[InlineData(ScriptSecretsType.Host)]
156-
[InlineData(ScriptSecretsType.Function)]
157-
public async Task BlobStorageRepo_WriteAsync_CreatesExpectedFile(ScriptSecretsType secretsType)
160+
[InlineData(SecretsRepositoryType.BlobStorage, ScriptSecretsType.Host)]
161+
[InlineData(SecretsRepositoryType.BlobStorage, ScriptSecretsType.Function)]
162+
[InlineData(SecretsRepositoryType.BlobStorageSas, ScriptSecretsType.Host)]
163+
[InlineData(SecretsRepositoryType.BlobStorageSas, ScriptSecretsType.Function)]
164+
public async Task BlobStorageRepo_WriteAsync_CreatesExpectedFile(SecretsRepositoryType repositoryType, ScriptSecretsType secretsType)
158165
{
159-
await WriteAsync_CreatesExpectedFile(SecretsRepositoryType.BlobStorage, secretsType);
166+
await WriteAsync_CreatesExpectedFile(repositoryType, secretsType);
160167
}
161168

162169
[Theory]
@@ -204,7 +211,7 @@ private async Task WriteAsync_CreatesExpectedFile(SecretsRepositoryType reposito
204211

205212
string filePath = Path.Combine(directory.Path, $"{testFunctionName ?? "host"}.json");
206213

207-
if (repositoryType == SecretsRepositoryType.BlobStorage)
214+
if (repositoryType == SecretsRepositoryType.BlobStorage || repositoryType == SecretsRepositoryType.BlobStorageSas)
208215
{
209216
Assert.True(_fixture.MarkerFileExists(testFunctionName ?? "host"));
210217
}
@@ -235,10 +242,12 @@ public async Task FileSystemRepo_WriteAsync_ChangeNotificationUpdatesExistingSec
235242
await WriteAsync_ChangeNotificationUpdatesExistingSecret(SecretsRepositoryType.FileSystem);
236243
}
237244

238-
[Fact]
239-
public async Task BlobStorageRepo_WriteAsync_ChangeNotificationUpdatesExistingSecret()
245+
[Theory]
246+
[InlineData(SecretsRepositoryType.BlobStorage)]
247+
[InlineData(SecretsRepositoryType.BlobStorageSas)]
248+
public async Task BlobStorageRepo_WriteAsync_ChangeNotificationUpdatesExistingSecret(SecretsRepositoryType repositoryType)
240249
{
241-
await WriteAsync_ChangeNotificationUpdatesExistingSecret(SecretsRepositoryType.BlobStorage);
250+
await WriteAsync_ChangeNotificationUpdatesExistingSecret(repositoryType);
242251
}
243252

244253
private async Task WriteAsync_ChangeNotificationUpdatesExistingSecret(SecretsRepositoryType repositoryType)
@@ -300,6 +309,8 @@ public async Task FileSystemRepo_PurgeOldSecrets_RemovesOldAndKeepsCurrentSecret
300309
[InlineData(SecretsRepositoryType.FileSystem, ScriptSecretsType.Function)]
301310
[InlineData(SecretsRepositoryType.BlobStorage, ScriptSecretsType.Host)]
302311
[InlineData(SecretsRepositoryType.BlobStorage, ScriptSecretsType.Function)]
312+
[InlineData(SecretsRepositoryType.BlobStorageSas, ScriptSecretsType.Host)]
313+
[InlineData(SecretsRepositoryType.BlobStorageSas, ScriptSecretsType.Function)]
303314
public async Task GetSecretSnapshots_ReturnsExpected(SecretsRepositoryType repositoryType, ScriptSecretsType secretsType)
304315
{
305316
using (var directory = new TempDirectory())
@@ -341,7 +352,6 @@ public Fixture()
341352
TestSiteName = "Test_test";
342353
var configuration = TestHelpers.GetTestConfiguration();
343354
BlobConnectionString = configuration.GetWebJobsConnectionString(ConnectionStringNames.Storage);
344-
BlobContainer = CloudStorageAccount.Parse(BlobConnectionString).CreateCloudBlobClient().GetContainerReference("azure-webjobs-secrets");
345355
KeyVaultConnectionString = configuration.GetWebJobsConnectionString(EnvironmentSettingNames.AzureWebJobsSecretStorageKeyVaultConnectionString);
346356
KeyVaultName = configuration.GetWebJobsConnectionString(EnvironmentSettingNames.AzureWebJobsSecretStorageKeyVaultName);
347357
AzureServiceTokenProvider azureServiceTokenProvider = new AzureServiceTokenProvider(KeyVaultConnectionString);
@@ -354,6 +364,8 @@ public Fixture()
354364

355365
public string BlobConnectionString { get; private set; }
356366

367+
public Uri BlobSasConnectionUri { get; private set; }
368+
357369
public CloudBlobContainer BlobContainer { get; private set; }
358370

359371
public KeyVaultClient KeyVaultClient { get; private set; }
@@ -373,6 +385,16 @@ public async Task TestInitialize(SecretsRepositoryType repositoryType, string se
373385
TestSiteName = testSiteName;
374386
}
375387

388+
if (RepositoryType == SecretsRepositoryType.BlobStorageSas)
389+
{
390+
BlobSasConnectionUri = await TestHelpers.CreateBlobContainerSas(BlobConnectionString, "azure-webjobs-secrets-sas");
391+
BlobContainer = new CloudBlobContainer(BlobSasConnectionUri);
392+
}
393+
else
394+
{
395+
BlobContainer = CloudStorageAccount.Parse(BlobConnectionString).CreateCloudBlobClient().GetContainerReference("azure-webjobs-secrets");
396+
}
397+
376398
await ClearAllBlobSecrets();
377399
ClearAllFileSecrets();
378400
await ClearAllKeyVaultSecrets();
@@ -384,6 +406,10 @@ public ISecretsRepository GetNewSecretRepository()
384406
{
385407
return new BlobStorageSecretsRepository(SecretsDirectory, BlobConnectionString, TestSiteName);
386408
}
409+
else if (RepositoryType == SecretsRepositoryType.BlobStorageSas)
410+
{
411+
return new BlobStorageSasSecretsRepository(SecretsDirectory, BlobSasConnectionUri.ToString(), TestSiteName);
412+
}
387413
else if (RepositoryType == SecretsRepositoryType.FileSystem)
388414
{
389415
return new FileSystemSecretsRepository(SecretsDirectory);
@@ -435,6 +461,7 @@ public async Task WriteSecret(string functionNameOrHost, ScriptSecrets scriptSec
435461
WriteSecretsToFile(functionNameOrHost, ScriptSecretSerializer.SerializeSecrets(scriptSecret));
436462
break;
437463
case SecretsRepositoryType.BlobStorage:
464+
case SecretsRepositoryType.BlobStorageSas:
438465
await WriteSecretsBlobAndUpdateSentinelFile(functionNameOrHost, ScriptSecretSerializer.SerializeSecrets(scriptSecret));
439466
break;
440467
case SecretsRepositoryType.KeyVault:
@@ -485,6 +512,7 @@ public async Task<ScriptSecrets> GetSecretText(string functionNameOrHost, Script
485512
secrets = ScriptSecretSerializer.DeserializeSecrets(type, secretText);
486513
break;
487514
case SecretsRepositoryType.BlobStorage:
515+
case SecretsRepositoryType.BlobStorageSas:
488516
secrets = await GetSecretBlobText(functionNameOrHost, type);
489517
break;
490518
case SecretsRepositoryType.KeyVault:
@@ -559,7 +587,13 @@ private string GetSecretName(string secretName)
559587

560588
private async Task ClearAllBlobSecrets()
561589
{
562-
await BlobContainer.CreateIfNotExistsAsync();
590+
// A sas connection requires the container to already exist, it
591+
// doesn't have permission to create it
592+
if (RepositoryType != SecretsRepositoryType.BlobStorageSas)
593+
{
594+
await BlobContainer.CreateIfNotExistsAsync();
595+
}
596+
563597
var blobs = await BlobContainer.ListBlobsSegmentedAsync(prefix: TestSiteName.ToLowerInvariant(), useFlatBlobListing: true,
564598
blobListingDetails: BlobListingDetails.None, maxResults: 100, currentToken: null, options: null, operationContext: null);
565599
foreach (IListBlobItem blob in blobs.Results)

test/WebJobs.Script.Tests.Shared/TestHelpers.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,5 +350,23 @@ public static async Task<Uri> CreateBlobSas(string connectionString, string file
350350

351351
return sasUri;
352352
}
353+
354+
public static async Task<Uri> CreateBlobContainerSas(string connectionString, string blobContainer)
355+
{
356+
CloudStorageAccount storageAccount = CloudStorageAccount.Parse(connectionString);
357+
var blobClient = storageAccount.CreateCloudBlobClient();
358+
var container = blobClient.GetContainerReference(blobContainer);
359+
await container.CreateIfNotExistsAsync();
360+
361+
var policy = new SharedAccessBlobPolicy
362+
{
363+
SharedAccessStartTime = DateTime.UtcNow,
364+
SharedAccessExpiryTime = DateTime.UtcNow.AddHours(1),
365+
Permissions = SharedAccessBlobPermissions.Read | SharedAccessBlobPermissions.Write | SharedAccessBlobPermissions.List | SharedAccessBlobPermissions.Delete
366+
};
367+
var sas = container.GetSharedAccessSignature(policy);
368+
369+
return new Uri(container.StorageUri.PrimaryUri, sas);
370+
}
353371
}
354372
}

0 commit comments

Comments
 (0)