From 4ff360109cdb2b7e7143963f02a75d8b0430a8ef Mon Sep 17 00:00:00 2001 From: Kamil Sobol <61715331+kasobol-msft@users.noreply.github.com> Date: Mon, 4 Jan 2021 16:41:55 -0800 Subject: [PATCH] Add AzureSasCredential (#17636) * Add AzureSasCredential * corner case. * api. * doc update. * less allocations. * call base last. * Revert "less allocations." This reverts commit 637560692b20fff151e8a6316df4946be1b286f3. * validation feedback. * policy rename. * another attempt to reduce allocations. * Revert "another attempt to reduce allocations." This reverts commit 89cc8672be2796a62e61f4c198f1605b7eb57b3f. * changelog. --- sdk/core/Azure.Core/CHANGELOG.md | 3 + sdk/core/Azure.Core/api/Azure.Core.net461.cs | 7 ++ sdk/core/Azure.Core/api/Azure.Core.net5.0.cs | 7 ++ .../api/Azure.Core.netstandard2.0.cs | 7 ++ sdk/core/Azure.Core/src/Azure.Core.csproj | 1 + sdk/core/Azure.Core/src/AzureSasCredential.cs | 60 +++++++++++++++ .../AzureSasCredentialSynchronousPolicy.cs | 41 ++++++++++ ...zureSasCredentialSynchronousPolicyTests.cs | 77 +++++++++++++++++++ sdk/core/Azure.Core/tests/PolicyTestBase.cs | 3 +- 9 files changed, 205 insertions(+), 1 deletion(-) create mode 100644 sdk/core/Azure.Core/src/AzureSasCredential.cs create mode 100644 sdk/core/Azure.Core/src/Shared/AzureSasCredentialSynchronousPolicy.cs create mode 100644 sdk/core/Azure.Core/tests/AzureSasCredentialSynchronousPolicyTests.cs diff --git a/sdk/core/Azure.Core/CHANGELOG.md b/sdk/core/Azure.Core/CHANGELOG.md index 776d4f0d69e5..c45a67d3054e 100644 --- a/sdk/core/Azure.Core/CHANGELOG.md +++ b/sdk/core/Azure.Core/CHANGELOG.md @@ -2,6 +2,9 @@ ## 1.8.0-beta.1 (Unreleased) +### Added +- `AzureSasCredential` and its respective policy. + ## 1.7.0 (2020-12-14) ### New Features diff --git a/sdk/core/Azure.Core/api/Azure.Core.net461.cs b/sdk/core/Azure.Core/api/Azure.Core.net461.cs index f22d63a15b29..5690e26096cc 100644 --- a/sdk/core/Azure.Core/api/Azure.Core.net461.cs +++ b/sdk/core/Azure.Core/api/Azure.Core.net461.cs @@ -22,6 +22,13 @@ public AzureKeyCredential(string key) { } public string Key { get { throw null; } } public void Update(string key) { } } + public partial class AzureSasCredential + { + public AzureSasCredential(string signature) { } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public string Signature { get { throw null; } } + public void Update(string signature) { } + } [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)] public readonly partial struct ETag : System.IEquatable { diff --git a/sdk/core/Azure.Core/api/Azure.Core.net5.0.cs b/sdk/core/Azure.Core/api/Azure.Core.net5.0.cs index bcabbc77fb10..e7a6b0df30ff 100644 --- a/sdk/core/Azure.Core/api/Azure.Core.net5.0.cs +++ b/sdk/core/Azure.Core/api/Azure.Core.net5.0.cs @@ -22,6 +22,13 @@ public AzureKeyCredential(string key) { } public string Key { get { throw null; } } public void Update(string key) { } } + public partial class AzureSasCredential + { + public AzureSasCredential(string signature) { } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public string Signature { get { throw null; } } + public void Update(string signature) { } + } [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)] public readonly partial struct ETag : System.IEquatable { diff --git a/sdk/core/Azure.Core/api/Azure.Core.netstandard2.0.cs b/sdk/core/Azure.Core/api/Azure.Core.netstandard2.0.cs index f22d63a15b29..5690e26096cc 100644 --- a/sdk/core/Azure.Core/api/Azure.Core.netstandard2.0.cs +++ b/sdk/core/Azure.Core/api/Azure.Core.netstandard2.0.cs @@ -22,6 +22,13 @@ public AzureKeyCredential(string key) { } public string Key { get { throw null; } } public void Update(string key) { } } + public partial class AzureSasCredential + { + public AzureSasCredential(string signature) { } + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] + public string Signature { get { throw null; } } + public void Update(string signature) { } + } [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)] public readonly partial struct ETag : System.IEquatable { diff --git a/sdk/core/Azure.Core/src/Azure.Core.csproj b/sdk/core/Azure.Core/src/Azure.Core.csproj index 144984d7964c..264d5baa6954 100644 --- a/sdk/core/Azure.Core/src/Azure.Core.csproj +++ b/sdk/core/Azure.Core/src/Azure.Core.csproj @@ -26,6 +26,7 @@ + diff --git a/sdk/core/Azure.Core/src/AzureSasCredential.cs b/sdk/core/Azure.Core/src/AzureSasCredential.cs new file mode 100644 index 000000000000..5248a7cb305c --- /dev/null +++ b/sdk/core/Azure.Core/src/AzureSasCredential.cs @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.ComponentModel; +using System.Threading; +using Azure.Core; + +namespace Azure +{ + /// + /// Shared access signature credential used to authenticate to an Azure Service. + /// It provides the ability to update the shared access signature without creating a new client. + /// + public class AzureSasCredential + { + private string _signature; + + /// + /// Shared access signature used to authenticate to an Azure service. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public string Signature + { + get => Volatile.Read(ref _signature); + private set => Volatile.Write(ref _signature, value); + } + + /// + /// Initializes a new instance of the class. + /// + /// Shared access signature to use to authenticate with the Azure service. + /// + /// Thrown when the is null. + /// + /// + /// Thrown when the is empty. + /// +#pragma warning disable CS8618 // Non-nullable field is uninitialized. Consider declaring as nullable. + public AzureSasCredential(string signature) => Update(signature); +#pragma warning restore CS8618 // Non-nullable field is uninitialized. Consider declaring as nullable. + + /// + /// Updates the shared access signature. + /// This is intended to be used when you've regenerated your shared access signature + /// and want to update long lived clients. + /// + /// Shared access signature to authenticate the service against. + /// + /// Thrown when the is null. + /// + /// + /// Thrown when the is empty. + /// + public void Update(string signature) + { + Argument.AssertNotNullOrWhiteSpace(signature, nameof(signature)); + Signature = signature; + } + } +} diff --git a/sdk/core/Azure.Core/src/Shared/AzureSasCredentialSynchronousPolicy.cs b/sdk/core/Azure.Core/src/Shared/AzureSasCredentialSynchronousPolicy.cs new file mode 100644 index 000000000000..8f45cc770f1c --- /dev/null +++ b/sdk/core/Azure.Core/src/Shared/AzureSasCredentialSynchronousPolicy.cs @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using Azure.Core.Pipeline; + +namespace Azure.Core +{ + internal class AzureSasCredentialSynchronousPolicy : HttpPipelineSynchronousPolicy + { + private readonly AzureSasCredential _credential; + + /// + /// Initializes a new instance of the class. + /// + /// The used to authenticate requests. + public AzureSasCredentialSynchronousPolicy(AzureSasCredential credential) + { + Argument.AssertNotNull(credential, nameof(credential)); + _credential = credential; + } + + /// + public override void OnSendingRequest(HttpMessage message) + { + string query = message.Request.Uri.Query; + string signature = _credential.Signature; + if (signature.StartsWith("?", StringComparison.InvariantCulture)) + { + signature = signature.Substring(1); + } + if (!query.Contains(signature)) + { + query = string.IsNullOrEmpty(query) ? '?' + signature : query + '&' + signature; + message.Request.Uri.Query = query; + } + + base.OnSendingRequest(message); + } + } +} diff --git a/sdk/core/Azure.Core/tests/AzureSasCredentialSynchronousPolicyTests.cs b/sdk/core/Azure.Core/tests/AzureSasCredentialSynchronousPolicyTests.cs new file mode 100644 index 000000000000..546b9cd339f3 --- /dev/null +++ b/sdk/core/Azure.Core/tests/AzureSasCredentialSynchronousPolicyTests.cs @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Threading; +using System.Threading.Tasks; +using Azure.Core.Pipeline; +using Azure.Core.TestFramework; +using NUnit.Framework; + +namespace Azure.Core.Tests +{ + public class AzureSasCredentialSynchronousPolicyTests : PolicyTestBase + { + [TestCase("sig=test_signature_value")] + [TestCase("?sig=test_signature_value")] + public async Task SetsSignatureEmptyQuery(string signatureValue) + { + var transport = new MockTransport(new MockResponse(200)); + var sasPolicy = new AzureSasCredentialSynchronousPolicy(new AzureSasCredential(signatureValue)); + + await SendGetRequest(transport, sasPolicy); + + Assert.AreEqual("?sig=test_signature_value", transport.SingleRequest.Uri.Query); + } + + [TestCase("sig=test_signature_value")] + [TestCase("?sig=test_signature_value")] + public async Task SetsSignatureNonEmptyQuery(string signatureValue) + { + var transport = new MockTransport(new MockResponse(200)); + var sasPolicy = new AzureSasCredentialSynchronousPolicy(new AzureSasCredential(signatureValue)); + string query = "?foo=bar"; + + await SendGetRequest(transport, sasPolicy, query: query); + + Assert.AreEqual($"?foo=bar&sig=test_signature_value", transport.SingleRequest.Uri.Query); + } + + [TestCase("sig=test_signature_value")] + [TestCase("?sig=test_signature_value")] + public async Task VerifyRetryEmptyQuery(string signatureValue) + { + var transport = new MockTransport(new MockResponse(200), new MockResponse(200)); + var sasPolicy = new AzureSasCredentialSynchronousPolicy(new AzureSasCredential(signatureValue)); + + using (Request request = transport.CreateRequest()) + { + request.Method = RequestMethod.Get; + var pipeline = new HttpPipeline(transport, new[] { sasPolicy }); + await pipeline.SendRequestAsync(request, CancellationToken.None); + await pipeline.SendRequestAsync(request, CancellationToken.None); + } + + Assert.AreEqual("?sig=test_signature_value", transport.Requests[0].Uri.Query); + } + + [TestCase("sig=test_signature_value")] + [TestCase("?sig=test_signature_value")] + public async Task VerifyRetryNonEmptyQuery(string signatureValue) + { + var transport = new MockTransport(new MockResponse(200), new MockResponse(200)); + var sasPolicy = new AzureSasCredentialSynchronousPolicy(new AzureSasCredential(signatureValue)); + string query = "?foo=bar"; + + using (Request request = transport.CreateRequest()) + { + request.Method = RequestMethod.Get; + request.Uri.Query = query; + var pipeline = new HttpPipeline(transport, new[] { sasPolicy }); + await pipeline.SendRequestAsync(request, CancellationToken.None); + await pipeline.SendRequestAsync(request, CancellationToken.None); + } + + Assert.AreEqual("?foo=bar&sig=test_signature_value", transport.Requests[0].Uri.Query); + } + } +} diff --git a/sdk/core/Azure.Core/tests/PolicyTestBase.cs b/sdk/core/Azure.Core/tests/PolicyTestBase.cs index 890d577ee871..e4aebef96861 100644 --- a/sdk/core/Azure.Core/tests/PolicyTestBase.cs +++ b/sdk/core/Azure.Core/tests/PolicyTestBase.cs @@ -11,7 +11,7 @@ namespace Azure.Core.Tests { public abstract class PolicyTestBase { - protected static async Task SendGetRequest(HttpPipelineTransport transport, HttpPipelinePolicy policy, ResponseClassifier responseClassifier = null) + protected static async Task SendGetRequest(HttpPipelineTransport transport, HttpPipelinePolicy policy, ResponseClassifier responseClassifier = null, string query = null) { Assert.IsInstanceOf(policy, "Use SyncAsyncPolicyTestBase base type for non-sync policies"); @@ -19,6 +19,7 @@ protected static async Task SendGetRequest(HttpPipelineTransport trans { request.Method = RequestMethod.Get; request.Uri.Reset(new Uri("http://example.com")); + request.Uri.Query = query; var pipeline = new HttpPipeline(transport, new[] { policy }, responseClassifier); return await pipeline.SendRequestAsync(request, CancellationToken.None); }