Skip to content

Commit 29cae32

Browse files
committed
Support for no storage account scenario.
1 parent 3b1c6ab commit 29cae32

File tree

10 files changed

+304
-42
lines changed

10 files changed

+304
-42
lines changed

release_notes.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
- My change description (#PR)
44
-->
55
- Add admin/host/config API (#7394)
6+
- Added support for no storage account scenarios (#8083)
67

78
**Release sprint:** Sprint 118
89
[ [bugs](https://github.com/Azure/azure-functions-host/issues?q=is%3Aissue+milestone%3A%22Functions+Sprint+<successiveSprint>%22+label%3Abug+is%3Aclosed) | [features](https://github.com/Azure/azure-functions-host/issues?q=is%3Aissue+milestone%3A%22Functions+Sprint+<successiveSprint>%22+label%3Afeature+is%3Aclosed) ]

src/WebJobs.Script.WebHost/Security/Authentication/Keys/AuthenticationLevelHandler.cs

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the MIT License. See License.txt in the project root for license information.
33

4-
using System;
54
using System.Collections.Generic;
65
using System.Linq;
76
using System.Security.Claims;
@@ -84,22 +83,25 @@ protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
8483

8584
internal static Task<(string, AuthorizationLevel)> GetAuthorizationKeyInfoAsync(HttpRequest request, ISecretManagerProvider secretManagerProvider)
8685
{
87-
// first see if a key value is specified via headers or query string (header takes precedence)
88-
string keyValue = null;
89-
if (request.Headers.TryGetValue(FunctionsKeyHeaderName, out StringValues values))
86+
if (secretManagerProvider.SecretsEnabled)
9087
{
91-
keyValue = values.First();
92-
}
93-
else if (request.Query.TryGetValue(FunctionsKeyQueryParamName, out values))
94-
{
95-
keyValue = values.First();
96-
}
88+
// first see if a key value is specified via headers or query string (header takes precedence)
89+
string keyValue = null;
90+
if (request.Headers.TryGetValue(FunctionsKeyHeaderName, out StringValues values))
91+
{
92+
keyValue = values.First();
93+
}
94+
else if (request.Query.TryGetValue(FunctionsKeyQueryParamName, out values))
95+
{
96+
keyValue = values.First();
97+
}
9798

98-
if (!string.IsNullOrEmpty(keyValue))
99-
{
100-
ISecretManager secretManager = secretManagerProvider.Current;
101-
var functionName = request.HttpContext.Features.Get<IFunctionExecutionFeature>()?.Descriptor.Name;
102-
return secretManager.GetAuthorizationLevelOrNullAsync(keyValue, functionName);
99+
if (!string.IsNullOrEmpty(keyValue))
100+
{
101+
ISecretManager secretManager = secretManagerProvider.Current;
102+
var functionName = request.HttpContext.Features.Get<IFunctionExecutionFeature>()?.Descriptor.Name;
103+
return secretManager.GetAuthorizationLevelOrNullAsync(keyValue, functionName);
104+
}
103105
}
104106

105107
return Task.FromResult<(string, AuthorizationLevel)>((null, AuthorizationLevel.Anonymous));

test/WebJobs.Script.Tests.Integration/TestFunctionHost.cs

Lines changed: 61 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System;
55
using System.Collections.Generic;
66
using System.Diagnostics;
7+
using System.IdentityModel.Tokens.Jwt;
78
using System.IO;
89
using System.Linq;
910
using System.Net.Http;
@@ -14,11 +15,12 @@
1415
using Microsoft.AspNetCore.Builder;
1516
using Microsoft.AspNetCore.Hosting;
1617
using Microsoft.AspNetCore.TestHost;
18+
using Microsoft.Azure.Web.DataProtection;
1719
using Microsoft.Azure.WebJobs.Host.Executors;
1820
using Microsoft.Azure.WebJobs.Script.ExtensionBundle;
19-
using Microsoft.Azure.WebJobs.Script.Grpc;
2021
using Microsoft.Azure.WebJobs.Script.Models;
2122
using Microsoft.Azure.WebJobs.Script.WebHost;
23+
using Microsoft.Azure.WebJobs.Script.WebHost.Authentication;
2224
using Microsoft.Azure.WebJobs.Script.WebHost.DependencyInjection;
2325
using Microsoft.Azure.WebJobs.Script.WebHost.Middleware;
2426
using Microsoft.Azure.WebJobs.Script.WebHost.Models;
@@ -32,6 +34,7 @@
3234
using Microsoft.Extensions.Logging;
3335
using Microsoft.Extensions.Logging.Abstractions;
3436
using Microsoft.Extensions.Options;
37+
using Microsoft.IdentityModel.Tokens;
3538
using Microsoft.WebJobs.Script.Tests;
3639
using Newtonsoft.Json.Linq;
3740
using IApplicationLifetime = Microsoft.AspNetCore.Hosting.IApplicationLifetime;
@@ -59,9 +62,10 @@ public TestFunctionHost(string scriptPath,
5962
Action<IWebJobsBuilder> configureScriptHostWebJobsBuilder = null,
6063
Action<IConfigurationBuilder> configureScriptHostAppConfiguration = null,
6164
Action<ILoggingBuilder> configureScriptHostLogging = null,
62-
Action<IServiceCollection> configureScriptHostServices = null)
65+
Action<IServiceCollection> configureScriptHostServices = null,
66+
Action<IConfigurationBuilder> configureWebHostAppConfiguration = null)
6367
: this(scriptPath, Path.Combine(Path.GetTempPath(), @"Functions"), configureWebHostServices, configureScriptHostWebJobsBuilder,
64-
configureScriptHostAppConfiguration, configureScriptHostLogging, configureScriptHostServices)
68+
configureScriptHostAppConfiguration, configureScriptHostLogging, configureScriptHostServices, configureWebHostAppConfiguration)
6569
{
6670
}
6771

@@ -70,7 +74,9 @@ public TestFunctionHost(string scriptPath, string logPath,
7074
Action<IWebJobsBuilder> configureScriptHostWebJobsBuilder = null,
7175
Action<IConfigurationBuilder> configureScriptHostAppConfiguration = null,
7276
Action<ILoggingBuilder> configureScriptHostLogging = null,
73-
Action<IServiceCollection> configureScriptHostServices = null)
77+
Action<IServiceCollection> configureScriptHostServices = null,
78+
Action<IConfigurationBuilder> configureWebHostAppConfiguration = null,
79+
bool addTestSettings = true)
7480
{
7581
_appRoot = scriptPath;
7682

@@ -123,7 +129,10 @@ public TestFunctionHost(string scriptPath, string logPath,
123129
})
124130
.ConfigureScriptHostAppConfiguration(scriptHostConfigurationBuilder =>
125131
{
126-
scriptHostConfigurationBuilder.AddTestSettings();
132+
if (addTestSettings)
133+
{
134+
scriptHostConfigurationBuilder.AddTestSettings();
135+
}
127136
configureScriptHostAppConfiguration?.Invoke(scriptHostConfigurationBuilder);
128137
})
129138
.ConfigureScriptHostLogging(scriptHostLoggingBuilder =>
@@ -146,7 +155,11 @@ public TestFunctionHost(string scriptPath, string logPath,
146155
}
147156

148157
config.Add(new ScriptEnvironmentVariablesConfigurationSource());
149-
config.AddTestSettings();
158+
if (addTestSettings)
159+
{
160+
config.AddTestSettings();
161+
}
162+
configureWebHostAppConfiguration?.Invoke(config);
150163
})
151164
.UseStartup<TestStartup>();
152165

@@ -182,7 +195,9 @@ public TestFunctionHost(string scriptPath, string logPath,
182195

183196
public ScriptJobHostOptions ScriptOptions => JobHostServices.GetService<IOptions<ScriptJobHostOptions>>().Value;
184197

185-
public ISecretManager SecretManager => _testServer.Host.Services.GetService<ISecretManagerProvider>().Current;
198+
public ISecretManagerProvider SecretManagerProvider => _testServer.Host.Services.GetService<ISecretManagerProvider>();
199+
200+
public ISecretManager SecretManager => SecretManagerProvider.Current;
186201

187202
public string LogPath => _hostOptions.LogPath;
188203

@@ -192,6 +207,11 @@ public TestFunctionHost(string scriptPath, string logPath,
192207

193208
public async Task<string> GetMasterKeyAsync()
194209
{
210+
if (!SecretManagerProvider.SecretsEnabled)
211+
{
212+
return null;
213+
}
214+
195215
HostSecretsInfo secrets = await SecretManager.GetHostSecretsAsync();
196216
return secrets.MasterKey;
197217
}
@@ -345,13 +365,44 @@ public async Task<FunctionStatus> GetFunctionStatusAsync(string functionName)
345365

346366
public async Task<HostStatus> GetHostStatusAsync()
347367
{
348-
HostSecretsInfo secrets = await SecretManager.GetHostSecretsAsync();
349-
string uri = $"admin/host/status?code={secrets.MasterKey}";
350-
HttpResponseMessage response = await HttpClient.GetAsync(uri);
368+
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, "admin/host/status");
369+
370+
if (SecretManagerProvider.SecretsEnabled)
371+
{
372+
// use admin key
373+
HostSecretsInfo secrets = await SecretManager.GetHostSecretsAsync();
374+
request.Headers.Add(AuthenticationLevelHandler.FunctionsKeyHeaderName, secrets.MasterKey);
375+
}
376+
else
377+
{
378+
// use admin jwt token
379+
string token = GenerateAdminJwtToken();
380+
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
381+
}
382+
383+
HttpResponseMessage response = await HttpClient.SendAsync(request);
351384
response.EnsureSuccessStatusCode();
352385
return await response.Content.ReadAsAsync<HostStatus>();
353386
}
354387

388+
public string GenerateAdminJwtToken()
389+
{
390+
var tokenHandler = new JwtSecurityTokenHandler();
391+
string defaultKey = Util.GetDefaultKeyValue();
392+
var key = Encoding.ASCII.GetBytes(defaultKey);
393+
var tokenDescriptor = new SecurityTokenDescriptor
394+
{
395+
Audience = string.Format(ScriptConstants.AdminJwtValidAudienceFormat, Environment.GetEnvironmentVariable(EnvironmentSettingNames.AzureWebsiteName)),
396+
Issuer = string.Format(ScriptConstants.AdminJwtValidIssuerFormat, Environment.GetEnvironmentVariable(EnvironmentSettingNames.AzureWebsiteName)),
397+
Expires = DateTime.UtcNow.AddHours(1),
398+
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
399+
};
400+
var token = tokenHandler.CreateToken(tokenDescriptor);
401+
string tokenHeaderValue = tokenHandler.WriteToken(token);
402+
403+
return tokenHeaderValue;
404+
}
405+
355406
public void Dispose()
356407
{
357408
if (!_isDisposed)
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"bindings": [
3+
{
4+
"type": "httpTrigger",
5+
"name": "input",
6+
"direction": "in",
7+
"methods": [ "get" ],
8+
"authLevel": "function"
9+
},
10+
{
11+
"type": "http",
12+
"name": "$return",
13+
"direction": "out"
14+
}
15+
]
16+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
public static string Run(dynamic input)
2+
{
3+
return $"Success";
4+
}

test/WebJobs.Script.Tests.Integration/TestScripts/CSharp/HttpTrigger-Scenarios/run.csx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
using System;
44
using System.Net;
5+
using System.Web;
56
using Newtonsoft.Json;
67
using Newtonsoft.Json.Linq;
78

@@ -13,6 +14,10 @@ public static async Task<HttpResponseMessage> Run(HttpRequestMessage req, Execut
1314

1415
switch (scenario)
1516
{
17+
case "swa":
18+
var query = HttpUtility.ParseQueryString(req.RequestUri.Query ?? string.Empty);
19+
var code = query["code"];
20+
return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(code) };
1621
case "appServiceFixupMiddleware":
1722
return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(req.RequestUri.ToString()) };
1823
case "appInsights-Success":

test/WebJobs.Script.Tests.Integration/WebHostEndToEnd/EndToEndTestFixture.cs

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -35,15 +35,21 @@ public abstract class EndToEndTestFixture : IAsyncLifetime
3535
private string _functionsWorkerRuntime;
3636
private int _workerProcessCount;
3737
private string _functionsWorkerRuntimeVersion;
38+
private bool _addTestSettings;
3839

39-
protected EndToEndTestFixture(string rootPath, string testId, string functionsWorkerRuntime, int workerProcessesCount = 1, string functionsWorkerRuntimeVersion = null)
40+
protected EndToEndTestFixture(string rootPath, string testId,
41+
string functionsWorkerRuntime,
42+
int workerProcessesCount = 1,
43+
string functionsWorkerRuntimeVersion = null,
44+
bool addTestSettings = true)
4045
{
4146
FixtureId = testId;
4247

4348
_rootPath = rootPath;
4449
_functionsWorkerRuntime = functionsWorkerRuntime;
4550
_workerProcessCount = workerProcessesCount;
4651
_functionsWorkerRuntimeVersion = functionsWorkerRuntimeVersion;
52+
_addTestSettings = addTestSettings;
4753
}
4854

4955
public CloudBlobContainer TestInputContainer { get; private set; }
@@ -126,7 +132,7 @@ string GetDestPath(int counter)
126132
FunctionsSyncManagerMock = new Mock<IFunctionsSyncManager>(MockBehavior.Strict);
127133
FunctionsSyncManagerMock.Setup(p => p.TrySyncTriggersAsync(It.IsAny<bool>())).ReturnsAsync(new SyncTriggersResult { Success = true });
128134

129-
Host = new TestFunctionHost(_copiedRootPath, logPath,
135+
Host = new TestFunctionHost(_copiedRootPath, logPath, addTestSettings: _addTestSettings,
130136
configureScriptHostWebJobsBuilder: webJobsBuilder =>
131137
{
132138
ConfigureScriptHost(webJobsBuilder);
@@ -135,23 +141,35 @@ string GetDestPath(int counter)
135141
{
136142
s.AddSingleton<IFunctionsSyncManager>(_ => FunctionsSyncManagerMock.Object);
137143
s.AddSingleton<IMetricsLogger>(_ => MetricsLogger);
144+
ConfigureScriptHost(s);
145+
},
146+
configureScriptHostAppConfiguration: configBuilder =>
147+
{
148+
ConfigureScriptHost(configBuilder);
138149
},
139150
configureWebHostServices: s =>
140151
{
141152
s.AddSingleton<IEventGenerator>(_ => EventGenerator);
142153
ConfigureWebHost(s);
154+
},
155+
configureWebHostAppConfiguration: configBuilder =>
156+
{
157+
ConfigureWebHost(configBuilder);
143158
});
144159

145160
string connectionString = Host.JobHostServices.GetService<IConfiguration>().GetWebJobsConnectionString(ConnectionStringNames.Storage);
146-
CloudStorageAccount storageAccount = CloudStorageAccount.Parse(connectionString);
161+
if (!string.IsNullOrEmpty(connectionString))
162+
{
163+
CloudStorageAccount storageAccount = CloudStorageAccount.Parse(connectionString);
147164

148-
QueueClient = storageAccount.CreateCloudQueueClient();
149-
BlobClient = storageAccount.CreateCloudBlobClient();
165+
QueueClient = storageAccount.CreateCloudQueueClient();
166+
BlobClient = storageAccount.CreateCloudBlobClient();
150167

151-
TableStorageAccount tableStorageAccount = TableStorageAccount.Parse(connectionString);
152-
TableClient = tableStorageAccount.CreateCloudTableClient();
168+
TableStorageAccount tableStorageAccount = TableStorageAccount.Parse(connectionString);
169+
TableClient = tableStorageAccount.CreateCloudTableClient();
153170

154-
await CreateTestStorageEntities();
171+
await CreateTestStorageEntities();
172+
}
155173

156174
MasterKey = await Host.GetMasterKeyAsync();
157175
}
@@ -160,10 +178,22 @@ public virtual void ConfigureScriptHost(IWebJobsBuilder webJobsBuilder)
160178
{
161179
}
162180

181+
public virtual void ConfigureScriptHost(IServiceCollection services)
182+
{
183+
}
184+
185+
public virtual void ConfigureScriptHost(IConfigurationBuilder configBuilder)
186+
{
187+
}
188+
163189
public virtual void ConfigureWebHost(IServiceCollection services)
164190
{
165191
}
166192

193+
public virtual void ConfigureWebHost(IConfigurationBuilder configBuilder)
194+
{
195+
}
196+
167197
public async Task<CloudQueue> GetNewQueue(string queueName)
168198
{
169199
var queue = QueueClient.GetQueueReference(string.Format("{0}-{1}", queueName, FixtureId));

0 commit comments

Comments
 (0)