Skip to content

Commit

Permalink
Added support for system-assigned identity to EventGrid output bindin…
Browse files Browse the repository at this point in the history
…gs (#36484)

* Added support for system-assigned identity to the EventGrid output bindings

* Updated to use the AzureComponentFactory.CreateCredential method

* Added unit tests and fixed issues discovered by tests

* Updated the bindings to look for a nested 'topicEndpointUri' property in the connection section

* Updated the api listing
  • Loading branch information
andrewjw1995 authored Jun 6, 2023
1 parent 5910ff8 commit a7063e4
Show file tree
Hide file tree
Showing 12 changed files with 366 additions and 122 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ namespace Microsoft.Azure.WebJobs.Extensions.EventGrid
public sealed partial class EventGridAttribute : System.Attribute
{
public EventGridAttribute() { }
public string Connection { get { throw null; } set { } }
[Microsoft.Azure.WebJobs.Description.AppSettingAttribute]
public string TopicEndpointUri { get { throw null; } set { } }
[Microsoft.Azure.WebJobs.Description.AppSettingAttribute]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using Azure;
using Azure.Core;
using Azure.Messaging.EventGrid;
using Microsoft.Extensions.Azure;
using Microsoft.Extensions.Configuration;
using System;

namespace Microsoft.Azure.WebJobs.Extensions.EventGrid.Config
{
internal class EventGridAsyncCollectorFactory
{
private const string TopicEndpointUri = "topicEndpointUri";

private readonly IConfiguration _configuration;
private readonly AzureComponentFactory _componentFactory;

protected EventGridAsyncCollectorFactory()
{ }

public EventGridAsyncCollectorFactory(IConfiguration configuration, AzureComponentFactory componentFactory)
{
_configuration = configuration;
_componentFactory = componentFactory;
}

internal void Validate(EventGridAttribute attribute)
{
if (attribute.TopicKeySetting != null)
{
if (!string.IsNullOrWhiteSpace(attribute.Connection))
{
throw new InvalidOperationException($"Conflicting topic credentials have been set in '{attribute.Connection}' and '{nameof(EventGridAttribute.TopicKeySetting)}'");
}

if (string.IsNullOrWhiteSpace(attribute.TopicKeySetting))
{
throw new InvalidOperationException($"The '{nameof(EventGridAttribute.TopicKeySetting)}' property must be the name of an application setting containing the Topic Key");
}
}

if (!string.IsNullOrWhiteSpace(attribute.Connection))
{
var connectionSection = _configuration.GetSection(attribute.Connection);
if (!connectionSection.Exists())
throw new InvalidOperationException($"The topic endpoint uri in '{attribute.Connection}' does not exist. " +
$"Make sure that it is a defined App Setting.");

var eventGridTopicUri = connectionSection[TopicEndpointUri];
if (!string.IsNullOrWhiteSpace(eventGridTopicUri))
{
if (!Uri.IsWellFormedUriString(eventGridTopicUri, UriKind.Absolute))
{
throw new InvalidOperationException($"The topic endpoint uri in '{attribute.Connection}' must be a valid absolute Uri");
}

if (!string.IsNullOrWhiteSpace(attribute.TopicEndpointUri) && eventGridTopicUri != attribute.TopicEndpointUri)
{
throw new InvalidOperationException($"Conflicting topic endpoint uris have been set in '{attribute.Connection}' and '{nameof(EventGridAttribute.TopicEndpointUri)}'");
}

return;
}
}

if (!string.IsNullOrWhiteSpace(attribute.TopicEndpointUri))
{
if (!Uri.IsWellFormedUriString(attribute.TopicEndpointUri, UriKind.Absolute))
{
throw new InvalidOperationException($"The '{nameof(EventGridAttribute.TopicEndpointUri)}' property must be a valid absolute Uri");
}

return;
}

throw new InvalidOperationException($"The '{nameof(EventGridAttribute.Connection)}.{TopicEndpointUri}' property or '{nameof(EventGridAttribute.TopicEndpointUri)}' property must be set");
}

internal virtual IAsyncCollector<object> CreateCollector(EventGridAttribute attribute)
=> new EventGridAsyncCollector(CreateClient(attribute));

private EventGridPublisherClient CreateClient(EventGridAttribute attribute)
{
var connectionInformation = ResolveConnectionInformation(attribute);
if (connectionInformation.AzureKeyCredential != null)
{
return new EventGridPublisherClient(connectionInformation.Endpoint, connectionInformation.AzureKeyCredential);
}
else
{
return new EventGridPublisherClient(connectionInformation.Endpoint, connectionInformation.TokenCredential);
}
}

internal EventGridConnectionInformation ResolveConnectionInformation(EventGridAttribute attribute)
{
if (!string.IsNullOrWhiteSpace(attribute.TopicKeySetting))
{
return new EventGridConnectionInformation(new Uri(attribute.TopicEndpointUri), new AzureKeyCredential(attribute.TopicKeySetting));
}

if (string.IsNullOrWhiteSpace(attribute.Connection))
{
var emptyConfiguration = new ConfigurationBuilder().Build();
return new EventGridConnectionInformation(new Uri(attribute.TopicEndpointUri), _componentFactory.CreateTokenCredential(emptyConfiguration));
}

var connectionSection = _configuration.GetSection(attribute.Connection);
var topicEndpointUri = connectionSection[TopicEndpointUri];
if (string.IsNullOrWhiteSpace(topicEndpointUri))
topicEndpointUri = attribute.TopicEndpointUri;

return new EventGridConnectionInformation(new Uri(topicEndpointUri), _componentFactory.CreateTokenCredential(connectionSection));
}

internal record EventGridConnectionInformation
{
public EventGridConnectionInformation(Uri endpoint, AzureKeyCredential azureKeyCredential)
{
Endpoint = endpoint;
AzureKeyCredential = azureKeyCredential;
}

public EventGridConnectionInformation(Uri endpoint, TokenCredential tokenCredential)
{
Endpoint = endpoint;
TokenCredential = tokenCredential;
}

public Uri Endpoint { get; }
public AzureKeyCredential AzureKeyCredential { get; }
public TokenCredential TokenCredential { get; }
}
}
}
Original file line number Diff line number Diff line change
@@ -1,28 +1,26 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using Azure;
using Azure.Core.Pipeline;
using Azure.Messaging;
using Azure.Messaging.EventGrid;
using Microsoft.Azure.WebJobs.Description;
using Microsoft.Azure.WebJobs.Host.Bindings;
using Microsoft.Azure.WebJobs.Host.Config;
using Microsoft.Azure.WebJobs.Host.Executors;
using Microsoft.Azure.WebJobs.Logging;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using System.Web;

namespace Microsoft.Azure.WebJobs.Extensions.EventGrid
namespace Microsoft.Azure.WebJobs.Extensions.EventGrid.Config
{
/// <summary>
/// Defines the configuration options for the EventGrid binding.
Expand All @@ -33,7 +31,7 @@ internal class EventGridExtensionConfigProvider : IExtensionConfigProvider,
{
private ILogger _logger;
private readonly ILoggerFactory _loggerFactory;
private readonly Func<EventGridAttribute, IAsyncCollector<object>> _converter;
private readonly EventGridAsyncCollectorFactory _collectorFactory;
private readonly HttpRequestProcessor _httpRequestProcessor;
private readonly DiagnosticScopeFactory _diagnosticScopeFactory;

Expand All @@ -42,22 +40,13 @@ internal class EventGridExtensionConfigProvider : IExtensionConfigProvider,
private const string ResourceProviderNamespace = "Microsoft.EventGrid";
private const string DiagnosticScopeName = "EventGrid.Process";

// for end to end testing
internal EventGridExtensionConfigProvider(
Func<EventGridAttribute, IAsyncCollector<object>> converter,
// default constructor
public EventGridExtensionConfigProvider(
EventGridAsyncCollectorFactory collectorFactory,
HttpRequestProcessor httpRequestProcessor,
ILoggerFactory loggerFactory)
{
_converter = converter;
_httpRequestProcessor = httpRequestProcessor;
_loggerFactory = loggerFactory;
_diagnosticScopeFactory = new DiagnosticScopeFactory(DiagnosticScopeNamespace, ResourceProviderNamespace, true, false);
}

// default constructor
public EventGridExtensionConfigProvider(HttpRequestProcessor httpRequestProcessor, ILoggerFactory loggerFactory)
{
_converter = (attr => new EventGridAsyncCollector(new EventGridPublisherClient(new Uri(attr.TopicEndpointUri), new AzureKeyCredential(attr.TopicKeySetting))));
_collectorFactory = collectorFactory;
_httpRequestProcessor = httpRequestProcessor;
_loggerFactory = loggerFactory;
_diagnosticScopeFactory = new DiagnosticScopeFactory(DiagnosticScopeNamespace, ResourceProviderNamespace, true, false);
Expand Down Expand Up @@ -98,21 +87,8 @@ public void Initialize(ExtensionConfigContext context)

// Register the output binding
var rule = context.AddBindingRule<EventGridAttribute>();
rule.BindToCollector(_converter);
rule.AddValidator((a, t) =>
{
// if app setting is missing, it will be caught by runtime
// this logic tries to validate the practicality of attribute properties
if (string.IsNullOrWhiteSpace(a.TopicKeySetting))
{
throw new InvalidOperationException($"The '{nameof(EventGridAttribute.TopicKeySetting)}' property must be the name of an application setting containing the Topic Key");
}
if (!Uri.IsWellFormedUriString(a.TopicEndpointUri, UriKind.Absolute))
{
throw new InvalidOperationException($"The '{nameof(EventGridAttribute.TopicEndpointUri)}' property must be a valid absolute Uri");
}
});
rule.BindToCollector(_collectorFactory.CreateCollector);
rule.AddValidator((a, t) => _collectorFactory.Validate(a));
}

private Dictionary<string, EventGridListener> _listeners = new Dictionary<string, EventGridListener>();
Expand All @@ -134,7 +110,7 @@ private async Task<HttpResponseMessage> ProcessAsync(HttpRequestMessage req)
// which requires webapi.core...but this does not work for .netframework2.0
// TODO change this once webjobs.script is migrated
var functionName = HttpUtility.ParseQueryString(req.RequestUri.Query)["functionName"];
if (String.IsNullOrEmpty(functionName) || !_listeners.TryGetValue(functionName, out EventGridListener listener))
if (string.IsNullOrEmpty(functionName) || !_listeners.TryGetValue(functionName, out EventGridListener listener))
{
_logger.LogInformation($"cannot find function: '{functionName}', available function names: [{string.Join(", ", _listeners.Keys.ToArray())}]");
return new HttpResponseMessage(HttpStatusCode.NotFound) { Content = new StringContent($"cannot find function: '{functionName}'") };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
// Licensed under the MIT License.

using System;
using Microsoft.Azure.WebJobs.Extensions.EventGrid.Config;
using Microsoft.Extensions.Azure;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;

namespace Microsoft.Azure.WebJobs.Extensions.EventGrid
Expand All @@ -22,7 +25,9 @@ public static IWebJobsBuilder AddEventGrid(this IWebJobsBuilder builder)
throw new ArgumentNullException(nameof(builder));
}

builder.Services.AddAzureClientsCore();
builder.Services.TryAddSingleton<HttpRequestProcessor>();
builder.Services.AddSingleton<EventGridAsyncCollectorFactory>();
builder.AddExtension<EventGridExtensionConfigProvider>();
return builder;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Azure.WebJobs" />
<PackageReference Include="Microsoft.Azure.WebJobs" />
<PackageReference Include="Microsoft.Extensions.Azure" />
<PackageReference Include="Azure.Messaging.EventGrid" />
</ItemGroup>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,10 @@ public sealed class EventGridAttribute : Attribute
/// <summary>Gets or sets the Topic Key setting. You can find information on getting the Key for a topic here: https://docs.microsoft.com/en-us/azure/event-grid/custom-event-quickstart#send-an-event-to-your-topic </summary>
[AppSetting]
public string TopicKeySetting { get; set; }

/// <summary>
/// Gets or sets the app setting name that contains the Event Grid topic's connection string.
/// </summary>
public string Connection { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System.Threading;
using System.Threading.Tasks;
using Microsoft.Azure.WebJobs.Extensions.EventGrid.Config;
using Microsoft.Azure.WebJobs.Host.Executors;

namespace Microsoft.Azure.WebJobs.Extensions.EventGrid
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using System.Threading.Tasks;
using Azure.Messaging;
using Azure.Messaging.EventGrid;
using Microsoft.Azure.WebJobs.Extensions.EventGrid.Config;
using Microsoft.Azure.WebJobs.Host.Bindings;
using Microsoft.Azure.WebJobs.Host.Listeners;
using Microsoft.Azure.WebJobs.Host.Protocols;
Expand Down
Loading

0 comments on commit a7063e4

Please sign in to comment.