-
Notifications
You must be signed in to change notification settings - Fork 0
/
AzureFunctionHostBuilderExtensions.cs
223 lines (196 loc) · 10.4 KB
/
AzureFunctionHostBuilderExtensions.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
namespace zyin.Extensions.AzureFunction.Configuration
{
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Microsoft.Azure.Functions.Extensions.DependencyInjection;
using Microsoft.Azure.KeyVault;
using Microsoft.Azure.Services.AppAuthentication;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Configuration.AzureKeyVault;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
/// <summary>
/// Extension class for IFunctionsHostBuilder to enable user secrets and KeyVault.
/// Use app id and app secret for local development and managed identity for prod.
/// </summary>
public static class AzureFunctionHostBuilderExtensions
{
/// <summary>
/// Key vault name settings key. It can be set using one of the following ways:
/// 1. local.settings.json for local development
/// 2. appsettings.{env}.json if you are using environment based appsettings.json
/// 3. App settings for real Azure environments (production/staging etc.)
/// </summary>
private static readonly string KeyVaultName = "KeyVaultName";
/// <summary>
/// Key vault app id - development only, should be set in user secrets.
/// </summary>
private static readonly string KeyVaultAppId = "KeyVaultAppId";
/// <summary>
/// Key vault app secret - development only, should be set in user secrets.
/// </summary>
private static readonly string KeyVaultAppSecret = "KeyVaultAppSecret";
/// <summary>
/// Add App settings, user secret and Azure key vault to FunctionHostBuilder's configuration builder.
/// 1.appsettings.json and a chain of appsettings.{environment}.json will be added to configuration. The chain is defined by environments parameter if provider.
/// 2.For development environment, asp.net core user secrets will be enabled.
/// 3.Azure key vault will be added if KeyVaultName is part of the settings (either app settings or appsettings.json)
/// For key vault, use app id and app secret from user secrets for local development; and managed identity for Azure environments.
/// </summary>
/// <typeparam name="T">Startup class</typeparam>
/// <param name="hostBuilder">host builder</param>
/// <param name="environments">optional environment list. If not defined only appsettings.json and a chain of appsettings.{environment}.json will be added.
/// If this paremeter is not null and it defines environment inheritance relationship, then appsettings.{parentenvironment}.json files are also added</param>
/// <returns>host builder</returns>
public static IFunctionsHostBuilder TryAddAppSettingsAndSecrets<T>(
this IFunctionsHostBuilder hostBuilder,
IEnumerable<HostEnvironment> environments = null)
where T: FunctionsStartup
{
if (hostBuilder == null)
{
throw new ArgumentNullException(nameof(hostBuilder));
}
ValidateEnvironments(environments);
// Get a config builder wrapping the predefined Azure Function config
var defaultConfig = GetDefaultAzureFunctionConfig(hostBuilder);
var configBuilder = new ConfigurationBuilder().AddConfiguration(defaultConfig);
// Add appsettings.*.json based on provided environment list
configBuilder.AddAppSettings<T>(environments);
// Add user secrets
if (HostEnvironment.IsDevelopment)
{
configBuilder.AddUserSecrets<T>();
}
// Try to add key vault
configBuilder.TryAddAzureKeyVault();
// Replace the configuration in DI container - this is a hack right now since
// FunctionHostBuilder doesn't provide a way to customize config builder
var newConfig = configBuilder.Build();
hostBuilder.Services.Replace(ServiceDescriptor.Singleton(typeof(IConfiguration), newConfig));
return hostBuilder;
}
/// <summary>
/// Add appsettings.json and appsettings.{environment}.json will be added to configuration chain. They are optional.
/// If environments parameter is provided, we'll use it for setting inheritance.
/// </summary>
/// <typeparam name="T">Startup class</typeparam>
/// <param name="configBuilder">config builder</param>
/// <param name="environments">environment list where you can define appsettings json inheritance</param>
/// <returns>host builder</returns>
private static IConfigurationBuilder AddAppSettings<T>(
this IConfigurationBuilder configBuilder,
IEnumerable<HostEnvironment> environments)
where T: FunctionsStartup
{
// Assembly containing Startup sits in the bin folder (e.g. home/site/wwwroot/bin).
// Setting files sits it's parent folder for both local and Azure environments.
var startupDirectory = Path.GetDirectoryName(typeof(T).Assembly.Location);
var settingsDirectory = Directory.GetParent(startupDirectory).FullName;
return configBuilder.SetBasePath(settingsDirectory).AddJsonFiles(environments);
}
/// <summary>
/// Add json file settings based on current environment and the specified
/// environment inheritance layers.
/// If environments param is null, only appsettings.json will be added.
/// </summary>
/// <param name="configBuilder">config builder</param>
/// <param name="environments">array of defined environments</param>
/// <returns>config builder</returns>
private static IConfigurationBuilder AddJsonFiles(
this IConfigurationBuilder configBuilder,
IEnumerable<HostEnvironment> environments)
{
var currentEnvName = HostEnvironment.Environment;
// Try to look the current environment up from the given environment list
var env = environments?.FirstOrDefault(e => string.Equals(e.Name, currentEnvName, StringComparison.OrdinalIgnoreCase));
if (env == null)
{
// If the current environment cannot be found from the environments list, we fall back to
// the .net core behavior by creating a temp environment using current environment's name.
env = new HostEnvironment(currentEnvName);
}
// Find the layering hierarchy (sigle direction list from children to parent), then reverse it (parent to children).
var layers = new List<HostEnvironment>();
while (env != null)
{
layers.Add(env);
env = env.Parent;
}
layers.Reverse();
// Add json files based on parent to child chains. Note appsettings.json is always included first.
configBuilder.AddJsonFile("appsettings.json", optional: true, reloadOnChange: false);
foreach (var environment in layers)
{
configBuilder.AddJsonFile($"appsettings.{environment.Name}.json", optional: true, reloadOnChange: false);
}
return configBuilder;
}
/// <summary>
/// Add Azure KeyVault using Managed identity.
/// </summary>
/// <param name="configBuilder">config builder</param>
/// <returns>config builder</returns>
private static IConfigurationBuilder TryAddAzureKeyVault(this IConfigurationBuilder configBuilder)
{
if (configBuilder == null)
{
throw new ArgumentNullException(nameof(configBuilder));
}
var tempConfig = configBuilder.Build();
var keyVaultName = tempConfig[KeyVaultName];
bool useKeyVault = !string.IsNullOrWhiteSpace(keyVaultName);
if (useKeyVault)
{
var keyVaultUrl = $"https://{keyVaultName}.vault.azure.net/";
if (HostEnvironment.IsDevelopment)
{
// Add Azure keyvault with app id and app secret from user secrets
var clientId = tempConfig[KeyVaultAppId];
var clientSecret = tempConfig[KeyVaultAppSecret];
configBuilder.AddAzureKeyVault(keyVaultUrl, clientId, clientSecret);
}
else
{
// Non-development environment. Add keyvault from managed identity
var azureServiceTokenProvider = new AzureServiceTokenProvider();
var keyVaultClient = new KeyVaultClient(new KeyVaultClient.AuthenticationCallback(azureServiceTokenProvider.KeyVaultTokenCallback));
configBuilder.AddAzureKeyVault(keyVaultUrl, keyVaultClient, new DefaultKeyVaultSecretManager());
}
}
return configBuilder;
}
/// <summary>
/// Get base configuration builder for Function app, by adding Function app original IConfiguration as config root.
/// This is a hack since Azure function host builder doesn't expose a way to customize ConfigurationBuilder.
/// </summary>
/// <param name="builder"host builder></param>
/// <returns>configuration builder</returns>
private static IConfiguration GetDefaultAzureFunctionConfig(IFunctionsHostBuilder builder)
{
return builder.Services.BuildServiceProvider().GetService<IConfiguration>();
}
/// <summary>
/// Validate the environments array to make sure environments are distinct
/// </summary>
/// <param name="environments">environment array</param>
private static void ValidateEnvironments(IEnumerable<HostEnvironment> environments)
{
if (environments == null || environments.Any())
{
return;
}
var envDict = new Dictionary<string, HostEnvironment>(StringComparer.OrdinalIgnoreCase);
foreach (var env in environments)
{
if (envDict.ContainsKey(env.Name))
{
throw new InvalidOperationException("Environment name is not unique in the given environments array");
}
envDict.Add(env.Name, env);
}
}
}
}