Skip to content

Commit

Permalink
Adding SAS Credential Support to the Event Grid SDK (Azure#13277)
Browse files Browse the repository at this point in the history
* Added SAS credential support and SAS token generation method in client

* Modified test sanitizer

* Added additional documentation
  • Loading branch information
kerri-lee committed Jul 7, 2020
1 parent 1629374 commit 2a64a67
Show file tree
Hide file tree
Showing 17 changed files with 425 additions and 168 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ public partial class EventGridClient
protected EventGridClient() { }
public EventGridClient(System.Uri endpoint, Azure.AzureKeyCredential credential) { }
public EventGridClient(System.Uri endpoint, Azure.AzureKeyCredential credential, Azure.Messaging.EventGrid.EventGridClientOptions options) { }
public EventGridClient(System.Uri endpoint, Azure.Messaging.EventGrid.SharedAccessSignatureCredential credential) { }
public EventGridClient(System.Uri endpoint, Azure.Messaging.EventGrid.SharedAccessSignatureCredential credential, Azure.Messaging.EventGrid.EventGridClientOptions options) { }
public string BuildSharedAccessSignature(System.DateTimeOffset expirationUtc) { throw null; }
public virtual Azure.Response PublishCloudEvents(System.Collections.Generic.IEnumerable<Azure.Messaging.EventGrid.Models.CloudEvent> events, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
public virtual System.Threading.Tasks.Task<Azure.Response> PublishCloudEventsAsync(System.Collections.Generic.IEnumerable<Azure.Messaging.EventGrid.Models.CloudEvent> events, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
public virtual Azure.Response PublishCustomEvents(System.Collections.Generic.IEnumerable<object> events, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
Expand All @@ -20,6 +23,11 @@ public enum ServiceVersion
V2018_01_01 = 1,
}
}
public partial class SharedAccessSignatureCredential
{
public SharedAccessSignatureCredential(string signature) { }
public string Signature { get { throw null; } }
}
}
namespace Azure.Messaging.EventGrid.Models
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Net;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using Azure.Core;
using Azure.Core.Pipeline;
using Azure.Messaging.EventGrid.Models;
Expand All @@ -20,35 +23,64 @@ public class EventGridClient
{
private readonly ServiceRestClient _serviceRestClient;
private readonly ClientDiagnostics _clientDiagnostics;
private readonly string _hostName;
private string _hostName => _endpoint.Host;
private readonly Uri _endpoint;
private readonly AzureKeyCredential _key;
private string _apiVersion;

/// <summary>Initalizes an instance of EventGridClient</summary>
protected EventGridClient()
{
}

/// <summary>Initalizes an instance of EventGridClient</summary>
/// <param name="endpoint">topic endpoint</param>
/// <param name="credential">used to connect to Azure</param>
/// <param name="endpoint">Topic endpoint</param>
/// <param name="credential">Credential used to connect to Azure</param>
public EventGridClient(Uri endpoint, AzureKeyCredential credential)
: this(endpoint, credential, new EventGridClientOptions())
{
}

/// <summary>Initalizes an instance of EventGridClient</summary>
/// <param name="endpoint">topic endpoint</param>
/// <param name="credential">used to connect to Azure</param>
/// <param name="options">configuring options</param>
/// <param name="endpoint">Topic endpoint</param>
/// <param name="credential">Credential used to connect to Azure</param>
public EventGridClient(Uri endpoint, SharedAccessSignatureCredential credential)
: this(endpoint, credential, new EventGridClientOptions())
{
}

/// <summary>Initalizes an instance of the<see cref="EventGridClient"/> class</summary>
/// <param name="endpoint">Topic endpoint</param>
/// <param name="credential">Credential used to connect to Azure</param>
/// <param name="options">Configuring options</param>
public EventGridClient(Uri endpoint, AzureKeyCredential credential, EventGridClientOptions options)
{
Argument.AssertNotNull(credential, nameof(credential));
options ??= new EventGridClientOptions();
_hostName = endpoint.Host;
_apiVersion = options.GetVersionString();
_endpoint = endpoint;
_key = credential;
HttpPipeline pipeline = HttpPipelineBuilder.Build(options, new AzureKeyCredentialPolicy(credential, Constants.SasKeyName));
_serviceRestClient = new ServiceRestClient(new ClientDiagnostics(options), pipeline, options.GetVersionString());
_clientDiagnostics = new ClientDiagnostics(options);
}

/// <summary>
/// Initializes a new instance of the <see cref="EventGridClient"/> class.
/// </summary>
/// <param name="endpoint">Topic endpoint</param>
/// <param name="credential">Credential used to connect to Azure</param>
/// <param name="options">Configuring options</param>
public EventGridClient(Uri endpoint, SharedAccessSignatureCredential credential, EventGridClientOptions options)
{
Argument.AssertNotNull(credential, nameof(credential));
options ??= new EventGridClientOptions();
_endpoint = endpoint;
HttpPipeline pipeline = HttpPipelineBuilder.Build(options, new SharedAccessSignatureCredentialPolicy(credential));
_serviceRestClient = new ServiceRestClient(new ClientDiagnostics(options), pipeline, options.GetVersionString());
_clientDiagnostics = new ClientDiagnostics(options);
}

/// <summary> Publishes a batch of EventGridEvents to an Azure Event Grid topic. </summary>
/// <param name="events"> An array of events to be published to Event Grid. </param>
/// <param name="cancellationToken"> The cancellation token to use. </param>
Expand Down Expand Up @@ -163,5 +195,39 @@ public virtual Response PublishCustomEvents(IEnumerable<object> events, Cancella
throw;
}
}

/// <summary>
/// Creates a SAS token for use with Event Grid service
/// </summary>
/// <param name="expirationUtc">Time at which the SAS token becomes invalid for authentication</param>
/// <returns>Returns the generated SAS token string</returns>
public string BuildSharedAccessSignature(DateTimeOffset expirationUtc)
{
const char Resource = 'r';
const char Expiration = 'e';
const char Signature = 's';

if (_key == null)
{
throw new NotSupportedException("Can only create a SAS token when using an EventGridClient created using AzureKeyCredential.");
}

var uriBuilder = new RequestUriBuilder();
uriBuilder.Reset(_endpoint);
uriBuilder.AppendQuery("api-version", _apiVersion, true);
string encodedResource = HttpUtility.UrlEncode(_endpoint.ToString());
var culture = CultureInfo.CreateSpecificCulture("en-US");
var encodedExpirationUtc = HttpUtility.UrlEncode(expirationUtc.ToString(culture));

string unsignedSas = $"{Resource}={encodedResource}&{Expiration}={encodedExpirationUtc}";
using (var hmac = new HMACSHA256(Convert.FromBase64String(_key.Key)))
{
string signature = Convert.ToBase64String(hmac.ComputeHash(Encoding.UTF8.GetBytes(unsignedSas)));
string encodedSignature = HttpUtility.UrlEncode(signature);
string signedSas = $"{unsignedSas}&{Signature}={encodedSignature}";

return signedSas;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.Text;

namespace Azure.Messaging.EventGrid
{
/// <summary>
/// SAS token used to authenticate to the Event Grid service.
/// </summary>
public class SharedAccessSignatureCredential
{
/// <summary>
/// Initializes a new instance of the <see cref="SharedAccessSignatureCredential"/> class.
/// </summary>
/// <param name="signature">SAS token used for authentication</param>
public SharedAccessSignatureCredential(string signature)
{
Signature = signature;
}
/// <summary>
/// SAS token used to authenticate to the Event Grid service.
/// </summary>
public string Signature { get; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.Text;
using Azure.Core;
using Azure.Core.Pipeline;

namespace Azure.Messaging.EventGrid
{
internal class SharedAccessSignatureCredentialPolicy : HttpPipelineSynchronousPolicy
{
private readonly SharedAccessSignatureCredential _credential;

/// <summary>
/// Initializes a new instance of the <see cref="SharedAccessSignatureCredentialPolicy"/> class.
/// </summary>
/// <param name="credential">The <see cref="SharedAccessSignatureCredential"/> used to authenticate requests.</param>
public SharedAccessSignatureCredentialPolicy(SharedAccessSignatureCredential credential)
{
Argument.AssertNotNull(credential, nameof(credential));
_credential = credential;
}

/// <inheritdoc/>
public override void OnSendingRequest(HttpMessage message)
{
base.OnSendingRequest(message);
message.Request.Headers.SetValue(Constants.SasTokenName, _credential.Signature);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using Azure.Core.TestFramework;
using Azure.Messaging.EventGrid;
using Azure.Messaging.EventGrid.Models;
using NUnit.Framework;

namespace Azure.Messaging.EventGrid.Tests
{
public class EventGridClientLiveTests : EventGridLiveTestBase
{
public EventGridClientLiveTests(bool async)
: base(async)
{
}

[Test]
public async Task CanPublishEvent()
{
EventGridClientOptions options = Recording.InstrumentClientOptions(new EventGridClientOptions());
EventGridClient client = InstrumentClient(
new EventGridClient(
new Uri(TestEnvironment.TopicHost),
new AzureKeyCredential(TestEnvironment.TopicKey),
options));
await client.PublishEventsAsync(GetEventsList());
}

[Test]
public async Task CanPublishEventToDomain()
{
EventGridClientOptions options = Recording.InstrumentClientOptions(new EventGridClientOptions());
EventGridClient client = InstrumentClient(
new EventGridClient(
new Uri(TestEnvironment.DomainHost),
new AzureKeyCredential(TestEnvironment.DomainKey),
options));
await client.PublishEventsAsync(GetEventsWithTopicsList());

}

[Test]
public async Task CanPublishCloudEvent()
{
EventGridClientOptions options = Recording.InstrumentClientOptions(new EventGridClientOptions());
EventGridClient client = InstrumentClient(
new EventGridClient(
new Uri(TestEnvironment.CloudEventTopicHost),
new AzureKeyCredential(TestEnvironment.CloudEventTopicKey),
options));
await client.PublishCloudEventsAsync(GetCloudEventsList());
}

[Test]
public async Task CanPublishEventUsingSAS()
{
EventGridClient client = new EventGridClient(
new Uri(TestEnvironment.TopicHost),
new AzureKeyCredential(TestEnvironment.TopicKey));

string sasToken = client.BuildSharedAccessSignature(DateTimeOffset.UtcNow.AddMinutes(60));

EventGridClient sasTokenClient = InstrumentClient(
new EventGridClient(
new Uri(TestEnvironment.TopicHost),
new SharedAccessSignatureCredential(sasToken),
Recording.InstrumentClientOptions(new EventGridClientOptions())));
await sasTokenClient.PublishEventsAsync(GetEventsList());
}

private IList<EventGridEvent> GetEventsList()
{
List<EventGridEvent> eventsList = new List<EventGridEvent>();

for (int i = 0; i < 10; i++)
{
eventsList.Add(
new EventGridEvent(
Recording.Random.NewGuid().ToString(),
$"Subject-{i}",
"hello",
"Microsoft.MockPublisher.TestEvent",
Recording.Now,
"1.0"));
}

return eventsList;
}

private IList<EventGridEvent> GetEventsWithTopicsList()
{
List<EventGridEvent> eventsList = new List<EventGridEvent>();

for (int i = 0; i < 10; i++)
{
eventsList.Add(
new EventGridEvent(
Recording.Random.NewGuid().ToString(),
$"Topic-{i}",
$"Subject-{i}",
"hello",
"Microsoft.MockPublisher.TestEvent",
Recording.Now,
"1",
"1.0"));
}

return eventsList;
}

private IList<CloudEvent> GetCloudEventsList()
{
List<CloudEvent> eventsList = new List<CloudEvent>();

for (int i = 0; i < 10; i++)
{
eventsList.Add(
new CloudEvent(
Recording.Random.NewGuid().ToString(),
$"Subject-{i}",
"record",
"1.0"));
}

return eventsList;
}

private IList<object> GetCustomEventsList()
{
List<object> eventsList = new List<object>();

for (int i = 0; i < 10; i++)
{
eventsList.Add(new TestEvent()
{
dataVersion = "1.0",
eventTime = Recording.Now,
eventType = "Microsoft.MockPublisher.TestEvent",
id = Recording.Random.NewGuid().ToString(),
subject = $"Subject-{i}",
topic = $"Topic-{i}"
});
}

return eventsList;
}
}
}
Loading

0 comments on commit 2a64a67

Please sign in to comment.