Skip to content

Commit 9b968f8

Browse files
committed
Added feature to use SAS uri for secret storage and supporting tests.
1 parent 92e98bf commit 9b968f8

File tree

9 files changed

+124
-27
lines changed

9 files changed

+124
-27
lines changed

src/WebJobs.Script.WebHost/GlobalSuppressions.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,4 +125,9 @@
125125
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Scope = "member", Target = "Microsoft.Azure.WebJobs.Script.WebHost.WebScriptHostManager.#.ctor(Microsoft.Azure.WebJobs.Script.ScriptHostConfiguration,Microsoft.Azure.WebJobs.Script.WebHost.ISecretManagerFactory,Microsoft.Azure.WebJobs.Script.Eventing.IScriptEventManager,Microsoft.Azure.WebJobs.Script.Config.ScriptSettingsManager,Microsoft.Azure.WebJobs.Script.WebHost.WebHostSettings,Microsoft.Extensions.Logging.ILoggerFactory,Microsoft.Azure.WebJobs.Script.IScriptHostFactory,Microsoft.Azure.WebJobs.Script.WebHost.ISecretsRepositoryFactory,Microsoft.Azure.WebJobs.Script.Scale.HostPerformanceManager,System.Int32,System.Int32)")]
126126
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2213:DisposableFieldsShouldBeDisposed", MessageId = "_syncTimer", Scope = "member", Target = "Microsoft.Azure.WebJobs.Script.WebHost.WebScriptHostManager.#Dispose(System.Boolean)")]
127127
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "config", Scope = "member", Target = "Microsoft.Azure.WebJobs.Script.WebHost.Extensions.FunctionMetadataExtensions.#GetTestData(System.String,Microsoft.Azure.WebJobs.Script.ScriptHostConfiguration)")]
128-
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors", Scope = "member", Target = "Microsoft.Azure.WebJobs.Script.WebHost.SecretManager.#.ctor(Microsoft.Azure.WebJobs.Script.WebHost.ISecretsRepository,Microsoft.Azure.WebJobs.Script.WebHost.IKeyValueConverterFactory,Microsoft.Azure.WebJobs.Host.TraceWriter,System.Boolean)")]
128+
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors", Scope = "member", Target = "Microsoft.Azure.WebJobs.Script.WebHost.SecretManager.#.ctor(Microsoft.Azure.WebJobs.Script.WebHost.ISecretsRepository,Microsoft.Azure.WebJobs.Script.WebHost.IKeyValueConverterFactory,Microsoft.Azure.WebJobs.Host.TraceWriter,System.Boolean)")]
129+
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Sas", Scope = "type", Target = "Microsoft.Azure.WebJobs.Script.WebHost.BlobStorageSasSecretsRepository")]
130+
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Sas", Scope = "member", Target = "Microsoft.Azure.WebJobs.Script.WebHost.BlobStorageSasSecretsRepository.#.ctor(System.String,System.String,System.String)")]
131+
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "1#", Scope = "member", Target = "Microsoft.Azure.WebJobs.Script.WebHost.BlobStorageSasSecretsRepository.#.ctor(System.String,System.String,System.String)")]
132+
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors", Scope = "member", Target = "Microsoft.Azure.WebJobs.Script.WebHost.BlobStorageSecretsRepository.#.ctor(System.String,System.String,System.String)")]
133+
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 the Azure blob storage backing store.
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/BlobStorageSecretsRepository.cs

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ namespace Microsoft.Azure.WebJobs.Script.WebHost
1818
/// <summary>
1919
/// An <see cref="ISecretsRepository"/> implementation that uses Azure blob storage as the backing store.
2020
/// </summary>
21-
public sealed class BlobStorageSecretsRepository : ISecretsRepository, IDisposable
21+
public class BlobStorageSecretsRepository : ISecretsRepository, IDisposable
2222
{
2323
private readonly string _secretsSentinelFilePath;
2424
private readonly string _secretsBlobPath;
@@ -57,15 +57,21 @@ public BlobStorageSecretsRepository(string secretSentinelDirectoryPath, string a
5757
_hostSecretsBlobPath = string.Format("{0}/{1}", _secretsBlobPath, ScriptConstants.HostMetadataFileName);
5858

5959
_accountConnectionString = accountConnectionString;
60-
CloudStorageAccount account = CloudStorageAccount.Parse(_accountConnectionString);
61-
CloudBlobClient client = account.CreateCloudBlobClient();
62-
63-
_blobContainer = client.GetContainerReference(_secretsContainerName);
64-
_blobContainer.CreateIfNotExists();
60+
_blobContainer = CreateBlobContainer(_accountConnectionString);
6561
}
6662

6763
public event EventHandler<SecretsChangedEventArgs> SecretsChanged;
6864

65+
protected virtual CloudBlobContainer CreateBlobContainer(string connectionString)
66+
{
67+
CloudStorageAccount account = CloudStorageAccount.Parse(connectionString);
68+
CloudBlobClient client = account.CreateCloudBlobClient();
69+
CloudBlobContainer blobContainer = client.GetContainerReference(_secretsContainerName);
70+
blobContainer.CreateIfNotExists();
71+
72+
return blobContainer;
73+
}
74+
6975
private string GetSecretsBlobPath(ScriptSecretsType secretsType, string functionName = null)
7076
{
7177
return secretsType == ScriptSecretsType.Host
@@ -182,6 +188,7 @@ private void Dispose(bool disposing)
182188
public void Dispose()
183189
{
184190
Dispose(true);
191+
GC.SuppressFinalize(this);
185192
}
186193
}
187194
}

src/WebJobs.Script.WebHost/Security/DefaultSecretsRepositoryFactory.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,13 @@ public ISecretsRepository Create(ScriptSettingsManager settingsManager, WebHostS
1717
{
1818
string secretStorageType = settingsManager.GetSetting(EnvironmentSettingNames.AzureWebJobsSecretStorageType);
1919
string storageString = AmbientConnectionStringProvider.Instance.GetConnectionString(ConnectionStringNames.Storage);
20-
if (secretStorageType != null && secretStorageType.Equals("Blob", StringComparison.OrdinalIgnoreCase) && storageString != null)
20+
string secretStorageSas = settingsManager.GetSetting(EnvironmentSettingNames.AzureWebJobsSecretStorageSas);
21+
if (secretStorageType != null && secretStorageType.Equals("Blob", StringComparison.OrdinalIgnoreCase) && secretStorageSas != null)
22+
{
23+
string siteSlotName = settingsManager.AzureWebsiteUniqueSlotName ?? config.HostConfig.HostId;
24+
return new BlobStorageSasSecretsRepository(Path.Combine(webHostSettings.SecretsPath, "Sentinels"), secretStorageSas, siteSlotName);
25+
}
26+
else if (secretStorageType != null && secretStorageType.Equals("Blob", StringComparison.OrdinalIgnoreCase) && storageString != null)
2127
{
2228
string siteSlotName = settingsManager.AzureWebsiteUniqueSlotName ?? config.HostConfig.HostId;
2329
return new BlobStorageSecretsRepository(Path.Combine(webHostSettings.SecretsPath, "Sentinels"), storageString, siteSlotName);

src/WebJobs.Script.WebHost/WebJobs.Script.WebHost.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -559,6 +559,7 @@
559559
<Compile Include="FunctionRequestInvoker.cs" />
560560
<Compile Include="ProxyFunctionExecutor.cs" />
561561
<Compile Include="Security\BlobStorageSecretsRepository.cs" />
562+
<Compile Include="Security\BlobStorageSasSecretsRepository.cs" />
562563
<Compile Include="Security\DefaultSecretManagerFactory.cs" />
563564
<Compile Include="Security\DefaultSecretsRepositoryFactory.cs" />
564565
<Compile Include="Security\FileSystemSecretsRepository.cs" />

src/WebJobs.Script/EnvironmentSettingNames.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ public static class EnvironmentSettingNames
2222
public const string AzureWebJobsExtensionsPath = "AzureWebJobs_ExtensionsPath";
2323
public const string AzureWebsiteAppCountersName = "WEBSITE_COUNTERS_APP";
2424
public const string AzureWebJobsSecretStorageType = "AzureWebJobsSecretStorageType";
25+
public const string AzureWebJobsSecretStorageSas = "AzureWebJobsSecretStorageSas";
2526
public const string AppInsightsInstrumentationKey = "APPINSIGHTS_INSTRUMENTATIONKEY";
2627
public const string ProxySiteExtensionEnabledKey = "ROUTING_EXTENSION_VERSION";
2728
public const string FunctionsExtensionVersion = "FUNCTIONS_EXTENSION_VERSION";

src/WebJobs.Script/GlobalSuppressions.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,4 +245,5 @@
245245
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Scope = "member", Target = "Microsoft.Azure.WebJobs.Script.FileTraceWriter.#.ctor(System.String,System.Diagnostics.TraceLevel,Microsoft.Azure.WebJobs.Script.Diagnostics.LogType)")]
246246
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Scope = "member", Target = "Microsoft.Azure.WebJobs.Script.ScriptHost.#ConfigureLogging()")]
247247
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Dir", Scope = "member", Target = "Microsoft.Azure.WebJobs.Script.Scale.ApplicationPerformanceCounters.#RemoteDirMonitorLimit")]
248-
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Dir", Scope = "member", Target = "Microsoft.Azure.WebJobs.Script.Scale.ApplicationPerformanceCounters.#RemoteDirMonitors")]
248+
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Dir", Scope = "member", Target = "Microsoft.Azure.WebJobs.Script.Scale.ApplicationPerformanceCounters.#RemoteDirMonitors")]
249+
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Sas", Scope = "member", Target = "Microsoft.Azure.WebJobs.Script.EnvironmentSettingNames.#AzureWebJobsSecretStorageSas")]

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

Lines changed: 52 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ public SecretsRepositoryTests(SecretsRepositoryTests.Fixture fixture)
2929
public enum SecretsRepositoryType
3030
{
3131
FileSystem,
32-
BlobStorage
32+
BlobStorage,
33+
BlobStorageSas
3334
}
3435

3536
[Fact]
@@ -38,10 +39,12 @@ public void FileSystemRepo_Constructor_CreatesSecretPathIfNotExists()
3839
Constructor_CreatesSecretPathIfNotExists(SecretsRepositoryType.FileSystem);
3940
}
4041

41-
[Fact]
42-
public void BlobStorageRepo_Constructor_CreatesSecretPathIfNotExists()
42+
[Theory]
43+
[InlineData(SecretsRepositoryType.BlobStorage)]
44+
[InlineData(SecretsRepositoryType.BlobStorageSas)]
45+
public void BlobStorageRepo_Constructor_CreatesSecretPathIfNotExists(SecretsRepositoryType repositoryType)
4346
{
44-
Constructor_CreatesSecretPathIfNotExists(SecretsRepositoryType.BlobStorage);
47+
Constructor_CreatesSecretPathIfNotExists(repositoryType);
4548
}
4649

4750
private void Constructor_CreatesSecretPathIfNotExists(SecretsRepositoryType repositoryType)
@@ -67,11 +70,13 @@ private void Constructor_CreatesSecretPathIfNotExists(SecretsRepositoryType repo
6770
}
6871

6972
[Theory]
70-
[InlineData(ScriptSecretsType.Host)]
71-
[InlineData(ScriptSecretsType.Function)]
72-
public async Task BlobStorageRepo_ReadAsync_ReadsExpectedFile(ScriptSecretsType secretsType)
73+
[InlineData(SecretsRepositoryType.BlobStorage, ScriptSecretsType.Host)]
74+
[InlineData(SecretsRepositoryType.BlobStorage, ScriptSecretsType.Function)]
75+
[InlineData(SecretsRepositoryType.BlobStorageSas, ScriptSecretsType.Host)]
76+
[InlineData(SecretsRepositoryType.BlobStorageSas, ScriptSecretsType.Function)]
77+
public async Task BlobStorageRepo_ReadAsync_ReadsExpectedFile(SecretsRepositoryType repositoryType, ScriptSecretsType secretsType)
7378
{
74-
await ReadAsync_ReadsExpectedFile(SecretsRepositoryType.BlobStorage, secretsType);
79+
await ReadAsync_ReadsExpectedFile(repositoryType, secretsType);
7580
}
7681

7782
[Theory]
@@ -101,11 +106,13 @@ private async Task ReadAsync_ReadsExpectedFile(SecretsRepositoryType repositoryT
101106
}
102107

103108
[Theory]
104-
[InlineData(ScriptSecretsType.Host)]
105-
[InlineData(ScriptSecretsType.Function)]
106-
public async Task BlobStorageRepo_WriteAsync_CreatesExpectedFile(ScriptSecretsType secretsType)
109+
[InlineData(SecretsRepositoryType.BlobStorage, ScriptSecretsType.Host)]
110+
[InlineData(SecretsRepositoryType.BlobStorage, ScriptSecretsType.Function)]
111+
[InlineData(SecretsRepositoryType.BlobStorageSas, ScriptSecretsType.Host)]
112+
[InlineData(SecretsRepositoryType.BlobStorageSas, ScriptSecretsType.Function)]
113+
public async Task BlobStorageRepo_WriteAsync_CreatesExpectedFile(SecretsRepositoryType repositoryType, ScriptSecretsType secretsType)
107114
{
108-
await WriteAsync_CreatesExpectedFile(SecretsRepositoryType.BlobStorage, secretsType);
115+
await WriteAsync_CreatesExpectedFile(repositoryType, secretsType);
109116
}
110117

111118
[Theory]
@@ -129,7 +136,7 @@ private async Task WriteAsync_CreatesExpectedFile(SecretsRepositoryType reposito
129136

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

132-
if (repositoryType == SecretsRepositoryType.BlobStorage)
139+
if (repositoryType == SecretsRepositoryType.BlobStorage || repositoryType == SecretsRepositoryType.BlobStorageSas)
133140
{
134141
Assert.True(_fixture.MarkerFileExists(testFunctionName ?? "host"));
135142
}
@@ -143,10 +150,12 @@ public async Task FileSystemRepo_WriteAsync_ChangeNotificationUpdatesExistingSec
143150
await WriteAsync_ChangeNotificationUpdatesExistingSecret(SecretsRepositoryType.FileSystem);
144151
}
145152

146-
[Fact]
147-
public async Task BlobStorageRepo_WriteAsync_ChangeNotificationUpdatesExistingSecret()
153+
[Theory]
154+
[InlineData(SecretsRepositoryType.BlobStorage)]
155+
[InlineData(SecretsRepositoryType.BlobStorageSas)]
156+
public async Task BlobStorageRepo_WriteAsync_ChangeNotificationUpdatesExistingSecret(SecretsRepositoryType repositoryType)
148157
{
149-
await WriteAsync_ChangeNotificationUpdatesExistingSecret(SecretsRepositoryType.BlobStorage);
158+
await WriteAsync_ChangeNotificationUpdatesExistingSecret(repositoryType);
150159
}
151160

152161
private async Task WriteAsync_ChangeNotificationUpdatesExistingSecret(SecretsRepositoryType repositoryType)
@@ -202,6 +211,8 @@ public async Task FileSystemRepo_PurgeOldSecrets_RemovesOldAndKeepsCurrentSecret
202211
[InlineData(SecretsRepositoryType.FileSystem, ScriptSecretsType.Function)]
203212
[InlineData(SecretsRepositoryType.BlobStorage, ScriptSecretsType.Host)]
204213
[InlineData(SecretsRepositoryType.BlobStorage, ScriptSecretsType.Function)]
214+
[InlineData(SecretsRepositoryType.BlobStorageSas, ScriptSecretsType.Host)]
215+
[InlineData(SecretsRepositoryType.BlobStorageSas, ScriptSecretsType.Function)]
205216
public async Task GetSecretSnapshots_ReturnsExpected(SecretsRepositoryType repositoryType, ScriptSecretsType secretsType)
206217
{
207218
using (var directory = new TempDirectory())
@@ -229,7 +240,6 @@ public Fixture()
229240
{
230241
TestSiteName = "TestSiteName";
231242
BlobConnectionString = AmbientConnectionStringProvider.Instance.GetConnectionString(ConnectionStringNames.Storage);
232-
BlobContainer = CloudStorageAccount.Parse(BlobConnectionString).CreateCloudBlobClient().GetContainerReference("azure-webjobs-secrets");
233243
}
234244

235245
public string TestSiteName { get; private set; }
@@ -238,6 +248,8 @@ public Fixture()
238248

239249
public string BlobConnectionString { get; private set; }
240250

251+
public Uri BlobSasConnectionUri { get; private set; }
252+
241253
public CloudBlobContainer BlobContainer { get; private set; }
242254

243255
public SecretsRepositoryType RepositoryType { get; private set; }
@@ -251,6 +263,16 @@ public void TestInitialize(SecretsRepositoryType repositoryType, string secretsD
251263
TestSiteName = testSiteName;
252264
}
253265

266+
if (RepositoryType == SecretsRepositoryType.BlobStorageSas)
267+
{
268+
BlobSasConnectionUri = TestHelpers.CreateBlobContainerSas(BlobConnectionString, "azure-webjobs-secrets-sas");
269+
BlobContainer = new CloudBlobContainer(BlobSasConnectionUri);
270+
}
271+
else
272+
{
273+
BlobContainer = CloudStorageAccount.Parse(BlobConnectionString).CreateCloudBlobClient().GetContainerReference("azure-webjobs-secrets");
274+
}
275+
254276
ClearAllBlobSecrets();
255277
ClearAllFileSecrets();
256278
}
@@ -261,6 +283,10 @@ public ISecretsRepository GetNewSecretRepository()
261283
{
262284
return new BlobStorageSecretsRepository(SecretsDirectory, BlobConnectionString, TestSiteName);
263285
}
286+
else if (RepositoryType == SecretsRepositoryType.BlobStorageSas)
287+
{
288+
return new BlobStorageSasSecretsRepository(SecretsDirectory, BlobSasConnectionUri.ToString(), TestSiteName);
289+
}
264290
return new FileSystemSecretsRepository(SecretsDirectory);
265291
}
266292

@@ -299,6 +325,7 @@ public void WriteSecret(string functionNameOrHost, string fileText)
299325
WriteSecretsToFile(functionNameOrHost, fileText);
300326
break;
301327
case SecretsRepositoryType.BlobStorage:
328+
case SecretsRepositoryType.BlobStorageSas:
302329
WriteSecretsBlobAndUpdateSentinelFile(functionNameOrHost, fileText);
303330
break;
304331
default:
@@ -336,6 +363,7 @@ public string GetSecretText(string functionNameOrHost)
336363
secretText = File.ReadAllText(SecretsFileOrSentinelPath(functionNameOrHost));
337364
break;
338365
case SecretsRepositoryType.BlobStorage:
366+
case SecretsRepositoryType.BlobStorageSas:
339367
secretText = GetSecretBlobText(functionNameOrHost);
340368
break;
341369
default:
@@ -374,7 +402,13 @@ private void ClearAllFileSecrets()
374402

375403
private void ClearAllBlobSecrets()
376404
{
377-
BlobContainer.CreateIfNotExists();
405+
// A sas connection requires the container to already exist, it
406+
// doesn't have permission to create it
407+
if (RepositoryType != SecretsRepositoryType.BlobStorageSas)
408+
{
409+
BlobContainer.CreateIfNotExists();
410+
}
411+
378412
var blobs = BlobContainer.ListBlobs(prefix: TestSiteName.ToLowerInvariant(), useFlatBlobListing: true);
379413
foreach (IListBlobItem blob in blobs)
380414
{

0 commit comments

Comments
 (0)