Skip to content

Commit 2050598

Browse files
committed
moving BlobStorageSecretsRepository to use UploadAsync instead of OpenWriteAsync
1 parent 1182b02 commit 2050598

File tree

2 files changed

+143
-4
lines changed

2 files changed

+143
-4
lines changed

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

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@
55
using System.Collections.Generic;
66
using System.IO;
77
using System.Threading.Tasks;
8+
using Azure;
89
using Azure.Storage.Blobs;
910
using Azure.Storage.Blobs.Models;
10-
using Azure.Storage.Blobs.Specialized;
1111
using Microsoft.Azure.WebJobs.Script.WebHost.Diagnostics.Extensions;
1212
using Microsoft.Extensions.Logging;
1313

@@ -187,11 +187,22 @@ private string GetSecretsBlobPath(ScriptSecretsType secretsType, string function
187187

188188
private async Task WriteToBlobAsync(string blobPath, string secretsContent)
189189
{
190-
BlockBlobClient secretBlobClient = Container.GetBlockBlobClient(blobPath);
191-
using (StreamWriter writer = new StreamWriter(await secretBlobClient.OpenWriteAsync(true)))
190+
BlobClient secretBlobClient = Container.GetBlobClient(blobPath);
191+
BlobUploadOptions uploadOptions = new BlobUploadOptions();
192+
193+
if (await secretBlobClient.ExistsAsync())
194+
{
195+
// Return a 412 if another write beats us to updating the file.
196+
BlobProperties properties = await secretBlobClient.GetPropertiesAsync();
197+
uploadOptions.Conditions = new BlobRequestConditions { IfMatch = properties.ETag };
198+
}
199+
else
192200
{
193-
await writer.WriteAsync(secretsContent);
201+
// Return a 409 if another write beats us to creating the file.
202+
uploadOptions.Conditions = new BlobRequestConditions { IfNoneMatch = ETag.All };
194203
}
204+
205+
await secretBlobClient.UploadAsync(BinaryData.FromString(secretsContent), uploadOptions);
195206
}
196207

197208
protected virtual void LogErrorMessage(string operation)

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

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using System.IO;
88
using System.Linq;
99
using System.Threading.Tasks;
10+
using Azure;
1011
using Microsoft.Azure.KeyVault;
1112
using Microsoft.Azure.KeyVault.Models;
1213
using Microsoft.Azure.Services.AppAuthentication;
@@ -369,6 +370,133 @@ public async Task GetSecretSnapshots_ReturnsExpected(SecretsRepositoryType repos
369370
}
370371
}
371372

373+
[Theory]
374+
[InlineData(SecretsRepositoryType.BlobStorage)]
375+
[InlineData(SecretsRepositoryType.BlobStorageSas)]
376+
public async Task BlobRepository_WriteAsync_DoesNot_ClearBlobContents(SecretsRepositoryType repositoryType)
377+
{
378+
using (var directory = new TempDirectory())
379+
{
380+
await _fixture.TestInitialize(repositoryType, directory.Path);
381+
382+
ScriptSecrets testSecrets = new HostSecrets()
383+
{
384+
MasterKey = new Key("master", "test"),
385+
FunctionKeys = new List<Key>() { new Key(KeyName, "test") },
386+
SystemKeys = new List<Key>() { new Key(KeyName, "test") }
387+
};
388+
389+
string testFunctionName = "host";
390+
391+
var target = _fixture.GetNewSecretRepository();
392+
393+
// Set up initial secrets.
394+
await _fixture.WriteSecret(testFunctionName, testSecrets);
395+
396+
// Perform a write and read similtaneously. Previously, our usage of OpenWriteAsync
397+
// would erase the content of the blob while writing, resulting in null secrets from the
398+
// read.
399+
Task writeTask = target.WriteAsync(ScriptSecretsType.Host, testFunctionName, testSecrets);
400+
HostSecrets secretsContent = await target.ReadAsync(ScriptSecretsType.Host, testFunctionName) as HostSecrets;
401+
402+
await writeTask;
403+
404+
Assert.Equal(secretsContent.MasterKey.Name, "master");
405+
Assert.Equal(secretsContent.MasterKey.Value, "test");
406+
Assert.Equal(secretsContent.FunctionKeys[0].Name, KeyName);
407+
Assert.Equal(secretsContent.FunctionKeys[0].Value, "test");
408+
Assert.Equal(secretsContent.SystemKeys[0].Name, KeyName);
409+
Assert.Equal(secretsContent.SystemKeys[0].Value, "test");
410+
}
411+
}
412+
413+
[Theory]
414+
[InlineData(SecretsRepositoryType.BlobStorage)]
415+
[InlineData(SecretsRepositoryType.BlobStorageSas)]
416+
public async Task BlobRepository_SimultaneousWrites_Throws_PreconditionFailed(SecretsRepositoryType repositoryType)
417+
{
418+
using (var directory = new TempDirectory())
419+
{
420+
await _fixture.TestInitialize(repositoryType, directory.Path);
421+
422+
HostSecrets testSecrets = new HostSecrets()
423+
{
424+
MasterKey = new Key("master", "test"),
425+
FunctionKeys = new List<Key>() { new Key(KeyName, "test") },
426+
SystemKeys = new List<Key>() { new Key(KeyName, "test") }
427+
};
428+
429+
string testFunctionName = "host";
430+
431+
var target = _fixture.GetNewSecretRepository();
432+
433+
// Set up initial secrets.
434+
await _fixture.WriteSecret(testFunctionName, testSecrets);
435+
HostSecrets secretsContent = await target.ReadAsync(ScriptSecretsType.Host, testFunctionName) as HostSecrets;
436+
Assert.Equal("test", secretsContent.FunctionKeys.Single().Value);
437+
438+
testSecrets.FunctionKeys.Single().Value = "changed";
439+
440+
// Simultaneous writes will result in one of the writes being discarded due to
441+
// non-matching ETag.
442+
Task writeTask1 = target.WriteAsync(ScriptSecretsType.Host, testFunctionName, testSecrets);
443+
Task writeTask2 = target.WriteAsync(ScriptSecretsType.Host, testFunctionName, testSecrets);
444+
445+
var ex = await Assert.ThrowsAsync<RequestFailedException>(() => Task.WhenAll(writeTask1, writeTask2));
446+
447+
// Ensure the write went through.
448+
secretsContent = await target.ReadAsync(ScriptSecretsType.Host, testFunctionName) as HostSecrets;
449+
Assert.Equal("changed", secretsContent.FunctionKeys.Single().Value);
450+
451+
Assert.Equal("ConditionNotMet", ex.ErrorCode);
452+
Assert.Equal(412, ex.Status);
453+
Assert.True(writeTask1.IsCompletedSuccessfully || writeTask2.IsCompletedSuccessfully,
454+
"One of the write operations should have completed successfully.");
455+
}
456+
}
457+
458+
[Theory]
459+
[InlineData(SecretsRepositoryType.BlobStorage)]
460+
[InlineData(SecretsRepositoryType.BlobStorageSas)]
461+
public async Task BlobRepository_SimultaneousCreates_Throws_Conflict(SecretsRepositoryType repositoryType)
462+
{
463+
using (var directory = new TempDirectory())
464+
{
465+
await _fixture.TestInitialize(repositoryType, directory.Path);
466+
467+
HostSecrets testSecrets = new HostSecrets()
468+
{
469+
MasterKey = new Key("master", "test"),
470+
FunctionKeys = new List<Key>() { new Key(KeyName, "test") },
471+
SystemKeys = new List<Key>() { new Key(KeyName, "test") }
472+
};
473+
474+
string testFunctionName = "host";
475+
476+
var target = _fixture.GetNewSecretRepository();
477+
478+
// Ensure nothing is there.
479+
HostSecrets secretsContent = await target.ReadAsync(ScriptSecretsType.Host, testFunctionName) as HostSecrets;
480+
Assert.Null(secretsContent);
481+
482+
// Simultaneous creates will result in one of the writes being discarded due to
483+
// non-matching ETag.
484+
Task writeTask1 = target.WriteAsync(ScriptSecretsType.Host, testFunctionName, testSecrets);
485+
Task writeTask2 = target.WriteAsync(ScriptSecretsType.Host, testFunctionName, testSecrets);
486+
487+
var ex = await Assert.ThrowsAsync<RequestFailedException>(() => Task.WhenAll(writeTask1, writeTask2));
488+
489+
// Ensure the write went through.
490+
secretsContent = await target.ReadAsync(ScriptSecretsType.Host, testFunctionName) as HostSecrets;
491+
Assert.Equal("test", secretsContent.FunctionKeys.Single().Value);
492+
493+
Assert.Equal("BlobAlreadyExists", ex.ErrorCode);
494+
Assert.Equal(409, ex.Status);
495+
Assert.True(writeTask1.IsCompletedSuccessfully || writeTask2.IsCompletedSuccessfully,
496+
"One of the write operations should have completed successfully.");
497+
}
498+
}
499+
372500
public class Fixture : IDisposable
373501
{
374502
public Fixture()

0 commit comments

Comments
 (0)