Skip to content

Commit b195ccc

Browse files
authored
Add support for storage token auth (Azure#15509)
1 parent 608f3fd commit b195ccc

18 files changed

+502
-64
lines changed

sdk/extensions/Microsoft.Extensions.Azure/api/Microsoft.Extensions.Azure.netstandard2.0.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,12 @@ public static partial class AzureClientServiceCollectionExtensions
2727
{
2828
public static void AddAzureClients(this Microsoft.Extensions.DependencyInjection.IServiceCollection collection, System.Action<Microsoft.Extensions.Azure.AzureClientFactoryBuilder> configureClients) { }
2929
}
30+
public abstract partial class AzureComponentFactory
31+
{
32+
protected AzureComponentFactory() { }
33+
public abstract object CreateClientOptions(System.Type optionsType, object serviceVersion, Microsoft.Extensions.Configuration.IConfiguration configuration);
34+
public abstract Azure.Core.TokenCredential CreateCredential(Microsoft.Extensions.Configuration.IConfiguration configuration);
35+
}
3036
public partial interface IAzureClientFactory<TClient>
3137
{
3238
TClient CreateClient(string name);

sdk/extensions/Microsoft.Extensions.Azure/src/AzureClientFactoryBuilder.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ internal AzureClientFactoryBuilder(IServiceCollection serviceCollection)
2626
_serviceCollection.AddOptions();
2727
_serviceCollection.TryAddSingleton<EventSourceLogForwarder>();
2828
_serviceCollection.TryAddSingleton(typeof(IAzureClientFactory<>), typeof(FallbackAzureClientFactory<>));
29+
_serviceCollection.TryAddSingleton<AzureComponentFactory, AzureComponentFactoryImpl>();
2930
}
3031

3132
IAzureClientBuilder<TClient, TOptions> IAzureClientFactoryBuilder.RegisterClientFactory<TClient, TOptions>(Func<TOptions, TClient> clientFactory)

sdk/extensions/Microsoft.Extensions.Azure/src/Internal/AzureClientFactory.cs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ internal class AzureClientFactory<TClient, TOptions>: IAzureClientFactory<TClien
1919
private readonly IOptionsMonitor<TOptions> _monitor;
2020

2121
private readonly EventSourceLogForwarder _logForwarder;
22+
private readonly AzureComponentFactory _componentFactory;
2223
private FallbackAzureClientFactory<TClient> _fallbackFactory;
2324

2425
public AzureClientFactory(
@@ -27,7 +28,8 @@ public AzureClientFactory(
2728
IOptionsMonitor<AzureClientCredentialOptions<TClient>> clientsOptions,
2829
IEnumerable<ClientRegistration<TClient>> clientRegistrations,
2930
IOptionsMonitor<TOptions> monitor,
30-
EventSourceLogForwarder logForwarder)
31+
EventSourceLogForwarder logForwarder,
32+
AzureComponentFactory componentFactory)
3133
{
3234
_clientRegistrations = new Dictionary<string, ClientRegistration<TClient>>();
3335
foreach (var registration in clientRegistrations)
@@ -40,6 +42,7 @@ public AzureClientFactory(
4042
_clientsOptions = clientsOptions;
4143
_monitor = monitor;
4244
_logForwarder = logForwarder;
45+
_componentFactory = componentFactory;
4346
}
4447

4548
public TClient CreateClient(string name)
@@ -48,8 +51,11 @@ public TClient CreateClient(string name)
4851

4952
if (!_clientRegistrations.TryGetValue(name, out ClientRegistration<TClient> registration))
5053
{
51-
_fallbackFactory ??= new FallbackAzureClientFactory<TClient>(_globalOptions, _serviceProvider, _logForwarder);
52-
54+
_fallbackFactory ??= new FallbackAzureClientFactory<TClient>(
55+
_globalOptions,
56+
_serviceProvider,
57+
_componentFactory,
58+
_logForwarder);
5359
return _fallbackFactory.CreateClient(name);
5460
}
5561

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using System;
5+
using Azure.Core;
6+
using Microsoft.Extensions.Configuration;
7+
8+
namespace Microsoft.Extensions.Azure
9+
{
10+
/// <summary>
11+
/// Exposes methods to create various Azure client related types.
12+
/// </summary>
13+
public abstract class AzureComponentFactory
14+
{
15+
/// <summary>
16+
/// Creates an instance of <see cref="TokenCredential"/> from the provided <see cref="IConfiguration"/> object or returns a current default.
17+
/// </summary>
18+
public abstract TokenCredential CreateCredential(IConfiguration configuration);
19+
20+
/// <summary>
21+
/// Creates an instance of a client options type while applying the global and configuration settings to it.
22+
/// </summary>
23+
/// <param name="optionsType">Type of the options.</param>
24+
/// <param name="serviceVersion">The value of ServiceVersion enum to use, null to use the default.</param>
25+
/// <param name="configuration">The <see cref="IConfiguration"/> instance to apply to options.</param>
26+
/// <returns>A new instance of <paramref name="optionsType"/>.</returns>
27+
public abstract object CreateClientOptions(Type optionsType, object serviceVersion, IConfiguration configuration);
28+
}
29+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using System;
5+
using Azure.Core;
6+
using Microsoft.Extensions.Configuration;
7+
using Microsoft.Extensions.Options;
8+
9+
namespace Microsoft.Extensions.Azure
10+
{
11+
internal class AzureComponentFactoryImpl: AzureComponentFactory
12+
{
13+
private readonly IServiceProvider _serviceProvider;
14+
private readonly IOptionsMonitor<AzureClientsGlobalOptions> _globalOptions;
15+
16+
public AzureComponentFactoryImpl(IOptionsMonitor<AzureClientsGlobalOptions> globalOptions, IServiceProvider serviceProvider)
17+
{
18+
_serviceProvider = serviceProvider;
19+
_globalOptions = globalOptions;
20+
}
21+
22+
/// <inheritdoc />
23+
public override TokenCredential CreateCredential(IConfiguration configuration)
24+
{
25+
return ClientFactory.CreateCredential(configuration) ?? _globalOptions.CurrentValue.CredentialFactory(_serviceProvider);
26+
}
27+
28+
public override object CreateClientOptions(Type optionsType, object serviceVersion, IConfiguration configuration)
29+
{
30+
var options = ClientFactory.CreateClientOptions(serviceVersion, optionsType);
31+
32+
if (options is ClientOptions clientOptions)
33+
{
34+
foreach (var globalConfigureOption in _globalOptions.CurrentValue.ConfigureOptionDelegates)
35+
{
36+
globalConfigureOption(clientOptions, _serviceProvider);
37+
}
38+
}
39+
configuration?.Bind(options);
40+
return options;
41+
}
42+
}
43+
}

sdk/extensions/Microsoft.Extensions.Azure/src/Internal/FallbackAzureClientFactory.cs

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,7 @@ namespace Microsoft.Extensions.Azure
1414
{
1515
internal class FallbackAzureClientFactory<TClient>: IAzureClientFactory<TClient>
1616
{
17-
private readonly AzureClientsGlobalOptions _globalOptions;
18-
private readonly IServiceProvider _serviceProvider;
17+
private readonly AzureComponentFactory _componentFactory;
1918
private readonly EventSourceLogForwarder _logForwarder;
2019
private readonly Dictionary<string, FallbackClientRegistration<TClient>> _clientRegistrations;
2120
private readonly Type _clientOptionType;
@@ -25,12 +24,12 @@ internal class FallbackAzureClientFactory<TClient>: IAzureClientFactory<TClient>
2524
public FallbackAzureClientFactory(
2625
IOptionsMonitor<AzureClientsGlobalOptions> globalOptions,
2726
IServiceProvider serviceProvider,
27+
AzureComponentFactory componentFactory,
2828
EventSourceLogForwarder logForwarder)
2929
{
30-
_serviceProvider = serviceProvider;
31-
_globalOptions = globalOptions.CurrentValue;
32-
_configurationRoot = _globalOptions.ConfigurationRootResolver?.Invoke(_serviceProvider);
30+
_configurationRoot = globalOptions.CurrentValue.ConfigurationRootResolver?.Invoke(serviceProvider);
3331

32+
_componentFactory = componentFactory;
3433
_logForwarder = logForwarder;
3534
_clientRegistrations = new Dictionary<string, FallbackClientRegistration<TClient>>();
3635

@@ -84,8 +83,7 @@ public TClient CreateClient(string name)
8483

8584
var currentOptions = _optionsFactory.CreateOptions(name);
8685
registration.Configuration.Bind(currentOptions);
87-
return registration.GetClient(currentOptions,
88-
ClientFactory.CreateCredential(registration.Configuration) ?? _globalOptions.CredentialFactory(_serviceProvider));
86+
return registration.GetClient(currentOptions, _componentFactory.CreateCredential(registration.Configuration));
8987
}
9088

9189

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using System.Collections.Generic;
5+
using System.Threading.Tasks;
6+
using Azure.Storage.Blobs.Specialized;
7+
using Azure.WebJobs.Extensions.Storage.Common.Tests;
8+
using Microsoft.Azure.WebJobs.Host.TestCommon;
9+
using Microsoft.Extensions.Azure;
10+
using Microsoft.Extensions.Configuration;
11+
using Microsoft.Extensions.Hosting;
12+
using Xunit;
13+
14+
namespace Microsoft.Azure.WebJobs.Host.FunctionalTests
15+
{
16+
[Collection(AzuriteCollection.Name)]
17+
public class BlobConfigurationTests
18+
{
19+
private readonly AzuriteFixture azuriteFixture;
20+
21+
public BlobConfigurationTests(AzuriteFixture azuriteFixture)
22+
{
23+
this.azuriteFixture = azuriteFixture;
24+
}
25+
26+
[Fact]
27+
public async Task BlobClient_CanConnect_ConnectionString()
28+
{
29+
var account = azuriteFixture.GetAzureAccount();
30+
var prog = new BindToCloudBlockBlobProgram();
31+
IHost host = new HostBuilder()
32+
.ConfigureAppConfiguration(cb =>
33+
{
34+
cb.AddInMemoryCollection(new Dictionary<string, string>()
35+
{
36+
{"CustomConnection", account.ConnectionString },
37+
{"blobPath", "cscontainer/csblob" }
38+
});
39+
})
40+
.ConfigureDefaultTestHost(prog, builder =>
41+
{
42+
SetupAzurite(builder);
43+
builder.AddAzureStorageBlobs().AddAzureStorageQueues();
44+
})
45+
.Build();
46+
47+
var jobHost = host.GetJobHost<BindToCloudBlockBlobProgram>();
48+
await jobHost.CallAsync(nameof(BindToCloudBlockBlobProgram.Run));
49+
50+
var result = prog.Result;
51+
52+
// Assert
53+
Assert.NotNull(result);
54+
Assert.Equal("csblob", result.Name);
55+
Assert.Equal("cscontainer", result.BlobContainerName);
56+
Assert.NotNull(result.BlobContainerName);
57+
Assert.False(await result.ExistsAsync());
58+
}
59+
60+
[Fact]
61+
public async Task BlobClient_CanConnect_EndPoint()
62+
{
63+
var account = azuriteFixture.GetAzureAccount();
64+
var prog = new BindToCloudBlockBlobProgram();
65+
IHost host = new HostBuilder()
66+
.ConfigureAppConfiguration(cb =>
67+
{
68+
cb.AddInMemoryCollection(new Dictionary<string, string>()
69+
{
70+
{"CustomConnection:endpoint", account.Endpoint },
71+
{"blobPath", "endpointcontainer/endpointblob" }
72+
});
73+
})
74+
.ConfigureDefaultTestHost(prog, builder =>
75+
{
76+
SetupAzurite(builder);
77+
builder.AddAzureStorageBlobs().AddAzureStorageQueues();
78+
})
79+
.Build();
80+
81+
var jobHost = host.GetJobHost<BindToCloudBlockBlobProgram>();
82+
await jobHost.CallAsync(nameof(BindToCloudBlockBlobProgram.Run));
83+
84+
var result = prog.Result;
85+
86+
// Assert
87+
Assert.NotNull(result);
88+
Assert.Equal("endpointblob", result.Name);
89+
Assert.Equal("endpointcontainer", result.BlobContainerName);
90+
Assert.NotNull(result.BlobContainerName);
91+
Assert.False(await result.ExistsAsync());
92+
}
93+
94+
private void SetupAzurite(IWebJobsBuilder builder)
95+
{
96+
builder.Services.AddAzureClients(builder =>
97+
{
98+
builder.UseCredential(azuriteFixture.GetCredential());
99+
builder.ConfigureDefaults(options => options.Transport = azuriteFixture.GetTransport());
100+
});
101+
}
102+
103+
104+
private class BindToCloudBlockBlobProgram
105+
{
106+
public BlockBlobClient Result { get; set; }
107+
108+
public void Run(
109+
[Blob("%blobPath%", Connection = "CustomConnection")] BlockBlobClient blob)
110+
{
111+
Result = blob;
112+
}
113+
}
114+
}
115+
}

sdk/storage/Azure.Storage.Webjobs.Extensions.Common/api/Azure.WebJobs.Extensions.Storage.Common.netstandard2.0.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ namespace Microsoft.Azure.WebJobs.Extensions.Storage.Common
22
{
33
public partial class StorageAccount
44
{
5-
public StorageAccount(string connectionString, Azure.Storage.Blobs.BlobClientOptions.ServiceVersion? blobServiceVersion = default(Azure.Storage.Blobs.BlobClientOptions.ServiceVersion?), Azure.Storage.Queues.QueueClientOptions.ServiceVersion? queueServiceVersion = default(Azure.Storage.Queues.QueueClientOptions.ServiceVersion?)) { }
5+
public StorageAccount(Azure.Storage.Blobs.BlobServiceClient blobServiceClient, Azure.Storage.Queues.QueueServiceClient queueServiceClient) { }
66
public virtual string Name { get { throw null; } }
77
public virtual Azure.Storage.Blobs.BlobServiceClient CreateBlobServiceClient() { throw null; }
88
public virtual Azure.Storage.Queues.QueueServiceClient CreateQueueServiceClient() { throw null; }
@@ -11,7 +11,7 @@ public partial class StorageAccount
1111
}
1212
public partial class StorageAccountProvider
1313
{
14-
public StorageAccountProvider(Microsoft.Extensions.Configuration.IConfiguration configuration) { }
14+
public StorageAccountProvider(Microsoft.Extensions.Configuration.IConfiguration configuration, Microsoft.Extensions.Azure.AzureComponentFactory componentFactory) { }
1515
public virtual Microsoft.Azure.WebJobs.Extensions.Storage.Common.StorageAccount Get(string name) { throw null; }
1616
public Microsoft.Azure.WebJobs.Extensions.Storage.Common.StorageAccount Get(string name, Microsoft.Azure.WebJobs.INameResolver resolver) { throw null; }
1717
public virtual Microsoft.Azure.WebJobs.Extensions.Storage.Common.StorageAccount GetHost() { throw null; }

sdk/storage/Azure.Storage.Webjobs.Extensions.Common/src/Azure.WebJobs.Extensions.Storage.Common.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
<ItemGroup>
2323
<!-- TODO (kasobol-msft) change this to PackageReference when stable -->
24+
<ProjectReference Include="..\..\..\extensions\Microsoft.Extensions.Azure\src\Microsoft.Extensions.Azure.csproj" />
2425
<ProjectReference Include="..\..\Azure.Storage.Blobs\src\Azure.Storage.Blobs.csproj" />
2526
<ProjectReference Include="..\..\Azure.Storage.Queues\src\Azure.Storage.Queues.csproj" />
2627
</ItemGroup>
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using Microsoft.Extensions.Configuration;
5+
6+
namespace Microsoft.Azure.WebJobs.Extensions.Storage.Common
7+
{
8+
internal static class IConfigurationExtensions
9+
{
10+
public static IConfigurationSection GetWebJobsConnectionStringSection(this IConfiguration configuration, string connectionStringName)
11+
{
12+
// first try prefixing
13+
string prefixedConnectionStringName = GetPrefixedConnectionStringName(connectionStringName);
14+
IConfigurationSection section = GetConnectionStringOrSetting(configuration, prefixedConnectionStringName);
15+
16+
if (!section.Exists())
17+
{
18+
// next try a direct unprefixed lookup
19+
section = GetConnectionStringOrSetting(configuration, connectionStringName);
20+
}
21+
22+
return section;
23+
}
24+
25+
public static string GetPrefixedConnectionStringName(string connectionStringName)
26+
{
27+
return Constants.WebJobsConfigurationSectionName + connectionStringName;
28+
}
29+
30+
/// <summary>
31+
/// Looks for a connection string by first checking the ConfigurationStrings section, and then the root.
32+
/// </summary>
33+
/// <param name="configuration">The configuration.</param>
34+
/// <param name="connectionName">The connection string key.</param>
35+
/// <returns></returns>
36+
public static IConfigurationSection GetConnectionStringOrSetting(this IConfiguration configuration, string connectionName)
37+
{
38+
var connectionStringSection = configuration?.GetSection("ConnectionStrings").GetSection(connectionName);
39+
40+
if (connectionStringSection.Exists())
41+
{
42+
return connectionStringSection;
43+
}
44+
return configuration?.GetSection(connectionName);
45+
}
46+
}
47+
}

0 commit comments

Comments
 (0)