Skip to content

Commit a18c301

Browse files
committed
Allowing Admin authenticated requests to bypass WebHook auth pipeline
1 parent 502ff9a commit a18c301

File tree

10 files changed

+198
-49
lines changed

10 files changed

+198
-49
lines changed

sample/HttpTrigger-CSharp/run.csx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
#r "System.Web.Http"
2-
3-
using System;
1+
using System;
42
using System.Linq;
53
using System.Net;
64
using System.Net.Http;
Lines changed: 13 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
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.Configuration;
6-
using System.IO;
7-
using System.Web;
5+
using System.Threading.Tasks;
86
using System.Web.Configuration;
97
using System.Web.Hosting;
108
using Autofac;
@@ -15,32 +13,12 @@ namespace WebJobs.Script.WebHost.App_Start
1513
{
1614
public static class AutofacBootstrap
1715
{
18-
internal static void Initialize(ContainerBuilder builder)
16+
internal static void Initialize(ContainerBuilder builder, WebHostSettings settings)
1917
{
20-
string logFilePath;
21-
string scriptRootPath;
22-
string secretsPath;
23-
string home = Environment.GetEnvironmentVariable("HOME");
24-
bool isLocal = string.IsNullOrEmpty(home);
25-
if (isLocal)
26-
{
27-
// we're running locally
28-
scriptRootPath = Path.Combine(HostingEnvironment.ApplicationPhysicalPath, @"..\..\sample");
29-
logFilePath = Path.Combine(Path.GetTempPath(), @"Functions");
30-
secretsPath = HttpContext.Current.Server.MapPath("~/App_Data/Secrets");
31-
}
32-
else
33-
{
34-
// we're running in Azure
35-
scriptRootPath = Path.Combine(home, @"site\wwwroot");
36-
logFilePath = Path.Combine(home, @"LogFiles\Application\Functions");
37-
secretsPath = Path.Combine(home, @"data\Functions\secrets");
38-
}
39-
4018
ScriptHostConfiguration scriptHostConfig = new ScriptHostConfiguration()
4119
{
42-
RootScriptPath = scriptRootPath,
43-
RootLogPath = logFilePath,
20+
RootScriptPath = settings.ScriptPath,
21+
RootLogPath = settings.LogPath,
4422
FileLoggingEnabled = true
4523
};
4624

@@ -55,15 +33,22 @@ internal static void Initialize(ContainerBuilder builder)
5533
WebScriptHostManager scriptHostManager = new WebScriptHostManager(scriptHostConfig);
5634
builder.RegisterInstance<WebScriptHostManager>(scriptHostManager);
5735

58-
SecretManager secretManager = new SecretManager(secretsPath);
36+
SecretManager secretManager = new SecretManager(settings.SecretsPath);
5937
// Make sure that host secrets get created on startup if they don't exist
6038
secretManager.GetHostSecrets();
6139
builder.RegisterInstance<SecretManager>(secretManager);
6240

6341
WebHookReceiverManager webHookReceiverManager = new WebHookReceiverManager(secretManager);
6442
builder.RegisterInstance<WebHookReceiverManager>(webHookReceiverManager);
6543

66-
HostingEnvironment.QueueBackgroundWorkItem((ct) => scriptHostManager.RunAndBlock(ct));
44+
if (!settings.IsSelfHost)
45+
{
46+
HostingEnvironment.QueueBackgroundWorkItem((ct) => scriptHostManager.RunAndBlock(ct));
47+
}
48+
else
49+
{
50+
Task.Run(() => scriptHostManager.RunAndBlock());
51+
}
6752
}
6853
}
6954
}

src/WebJobs.Script.WebHost/App_Start/WebApiConfig.cs

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
// Licensed under the MIT License. See License.txt in the project root for license information.
33

44
using System;
5+
using System.IO;
6+
using System.Web;
7+
using System.Web.Hosting;
58
using System.Web.Http;
69
using Autofac;
710
using Autofac.Integration.WebApi;
@@ -14,19 +17,28 @@ namespace WebJobs.Script.WebHost
1417
public static class WebApiConfig
1518
{
1619
public static void Register(HttpConfiguration config)
20+
{
21+
Register(config, GetDefaultSettings());
22+
}
23+
24+
public static void Register(HttpConfiguration config, WebHostSettings settings = null)
1725
{
1826
if (config == null)
1927
{
2028
throw new ArgumentNullException("config");
2129
}
30+
if (settings == null)
31+
{
32+
throw new ArgumentNullException("settings");
33+
}
2234

2335
var builder = new ContainerBuilder();
2436
builder.RegisterApiControllers(typeof(FunctionsController).Assembly);
25-
AutofacBootstrap.Initialize(builder);
37+
AutofacBootstrap.Initialize(builder, settings);
2638
var container = builder.Build();
27-
GlobalConfiguration.Configuration.DependencyResolver = new AutofacWebApiDependencyResolver(container);
39+
config.DependencyResolver = new AutofacWebApiDependencyResolver(container);
2840

29-
config.MessageHandlers.Add(new EnsureHostRunningHandler());
41+
config.MessageHandlers.Add(new EnsureHostRunningHandler(config));
3042

3143
// Web API configuration and services
3244

@@ -59,5 +71,28 @@ public static void Register(HttpConfiguration config)
5971
config.InitializeReceiveGitHubWebHooks();
6072
config.InitializeReceiveSalesforceWebHooks();
6173
}
74+
75+
private static WebHostSettings GetDefaultSettings()
76+
{
77+
WebHostSettings settings = new WebHostSettings();
78+
79+
string home = Environment.GetEnvironmentVariable("HOME");
80+
bool isLocal = string.IsNullOrEmpty(home);
81+
if (isLocal)
82+
{
83+
settings.ScriptPath = Path.Combine(HostingEnvironment.ApplicationPhysicalPath, @"..\..\sample");
84+
settings.LogPath = Path.Combine(Path.GetTempPath(), @"Functions");
85+
settings.SecretsPath = HttpContext.Current.Server.MapPath("~/App_Data/Secrets");
86+
}
87+
else
88+
{
89+
// we're running in Azure
90+
settings.ScriptPath = Path.Combine(home, @"site\wwwroot");
91+
settings.LogPath = Path.Combine(home, @"LogFiles\Application\Functions");
92+
settings.SecretsPath = Path.Combine(home, @"data\Functions\secrets");
93+
}
94+
95+
return settings;
96+
}
6297
}
6398
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT License. See License.txt in the project root for license information.
3+
4+
namespace WebJobs.Script.WebHost
5+
{
6+
public class WebHostSettings
7+
{
8+
public bool IsSelfHost { get; set; }
9+
public string ScriptPath { get; set; }
10+
public string LogPath { get; set; }
11+
public string SecretsPath { get; set; }
12+
}
13+
}

src/WebJobs.Script.WebHost/Controllers/FunctionsController.cs

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using System.Threading.Tasks;
1010
using System.Web.Http;
1111
using System.Web.Http.Controllers;
12+
using Microsoft.Azure.WebJobs.Script;
1213
using Microsoft.Azure.WebJobs.Script.Description;
1314
using WebJobs.Script.WebHost.Filters;
1415
using WebJobs.Script.WebHost.WebHooks;
@@ -40,29 +41,41 @@ public override async Task<HttpResponseMessage> ExecuteAsync(HttpControllerConte
4041
return new HttpResponseMessage(HttpStatusCode.NotFound);
4142
}
4243

43-
// Authorize the request
44+
// Determine the authorization level of the request
4445
SecretManager secretManager = (SecretManager)controllerContext.Configuration.DependencyResolver.GetService(typeof(SecretManager));
46+
AuthorizationLevel authorizationLevel = AuthorizationLevelAttribute.GetAuthorizationLevel(request, secretManager, functionName: function.Name);
47+
48+
// Dispatch the request
4549
HttpTriggerBindingMetadata httpFunctionMetadata = (HttpTriggerBindingMetadata)function.Metadata.InputBindings.FirstOrDefault(p => p.Type == BindingType.HttpTrigger);
4650
bool isWebHook = !string.IsNullOrEmpty(httpFunctionMetadata.WebHookType);
47-
if (!isWebHook && !AuthorizationLevelAttribute.IsAuthorized(request, httpFunctionMetadata.AuthLevel, secretManager, functionName: function.Name))
48-
{
49-
return new HttpResponseMessage(HttpStatusCode.Unauthorized);
50-
}
51-
5251
HttpResponseMessage response = null;
5352
if (isWebHook)
5453
{
55-
// This is a WebHook request so define a delegate for the user function.
56-
// The WebHook Receiver pipeline will first validate the request fully
57-
// then invoke this callback.
58-
Func<HttpRequestMessage, Task<HttpResponseMessage>> invokeFunction = async (req) =>
54+
if (authorizationLevel == AuthorizationLevel.Admin)
55+
{
56+
// Admin level requests bypass the WebHook auth pipeline
57+
response = await _scriptHostManager.HandleRequestAsync(function, request, cancellationToken);
58+
}
59+
else
5960
{
60-
return await _scriptHostManager.HandleRequestAsync(function, req, cancellationToken);
61-
};
62-
response = await _webHookReceiverManager.HandleRequestAsync(function, request, invokeFunction);
61+
// This is a WebHook request so define a delegate for the user function.
62+
// The WebHook Receiver pipeline will first validate the request fully
63+
// then invoke this callback.
64+
Func<HttpRequestMessage, Task<HttpResponseMessage>> invokeFunction = async (req) =>
65+
{
66+
return await _scriptHostManager.HandleRequestAsync(function, req, cancellationToken);
67+
};
68+
response = await _webHookReceiverManager.HandleRequestAsync(function, request, invokeFunction);
69+
}
6370
}
6471
else
6572
{
73+
// Authorize
74+
if (authorizationLevel < httpFunctionMetadata.AuthLevel)
75+
{
76+
return new HttpResponseMessage(HttpStatusCode.Unauthorized);
77+
}
78+
6679
// Validate the HttpMethod
6780
// Note that for WebHook requests, WebHook receiver does its own validation
6881
if (httpFunctionMetadata.Methods != null && !httpFunctionMetadata.Methods.Contains(request.Method))
574 Bytes
Binary file not shown.

src/WebJobs.Script.WebHost/Handlers/EnsureHostRunningHandler.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,14 @@ public class EnsureHostRunningHandler : DelegatingHandler
1616
private readonly int _hostRunningPollIntervalMs = 500;
1717
private WebScriptHostManager _scriptHostManager;
1818

19-
public EnsureHostRunningHandler()
19+
public EnsureHostRunningHandler(HttpConfiguration config)
2020
{
21-
_scriptHostManager = (WebScriptHostManager)GlobalConfiguration.Configuration.DependencyResolver.GetService(typeof(WebScriptHostManager));
21+
if (config == null)
22+
{
23+
throw new ArgumentNullException("config");
24+
}
25+
26+
_scriptHostManager = (WebScriptHostManager)config.DependencyResolver.GetService(typeof(WebScriptHostManager));
2227
}
2328

2429
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)

src/WebJobs.Script.WebHost/WebJobs.Script.WebHost.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,7 @@
233233
<ItemGroup>
234234
<Compile Include="App_Start\AutofacBootstrap.cs" />
235235
<Compile Include="App_Start\WebApiConfig.cs" />
236+
<Compile Include="App_Start\WebHostSettings.cs" />
236237
<Compile Include="Controllers\AdminController.cs" />
237238
<Compile Include="Controllers\FunctionsController.cs" />
238239
<Compile Include="Controllers\HomeController.cs" />
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT License. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.IO;
6+
using System.Net;
7+
using System.Net.Http;
8+
using System.Net.Http.Headers;
9+
using System.Threading.Tasks;
10+
using System.Web.Http;
11+
using WebJobs.Script.WebHost;
12+
using Xunit;
13+
14+
namespace WebJobs.Script.Tests
15+
{
16+
public class SamplesEndToEndTests : IClassFixture<SamplesEndToEndTests.TestFixture>
17+
{
18+
private TestFixture _fixture;
19+
20+
public SamplesEndToEndTests(TestFixture fixture)
21+
{
22+
_fixture = fixture;
23+
}
24+
25+
[Fact]
26+
public async Task Home_Get_Succeeds()
27+
{
28+
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, string.Empty);
29+
30+
HttpResponseMessage response = await this._fixture.Client.SendAsync(request);
31+
Assert.Equal(HttpStatusCode.NoContent, response.StatusCode);
32+
}
33+
34+
[Fact]
35+
public async Task HttpTrigger_Get_Succeeds()
36+
{
37+
string uri = "api/httptrigger?code=hyexydhln844f2mb7hgsup2yf8dowlb0885mbiq1&name=Mathew";
38+
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, uri);
39+
40+
HttpResponseMessage response = await this._fixture.Client.SendAsync(request);
41+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
42+
string body = await response.Content.ReadAsStringAsync();
43+
Assert.Equal("Hello Mathew", body);
44+
}
45+
46+
[Fact]
47+
public async Task GenericWebHook_Post_Succeeds()
48+
{
49+
string uri = "api/webhook-generic?code=1388a6b0d05eca2237f10e4a4641260b0a08f3a5";
50+
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, uri);
51+
request.Content = new StringContent("{ 'a': 'Foobar' }");
52+
request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
53+
54+
HttpResponseMessage response = await this._fixture.Client.SendAsync(request);
55+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
56+
string body = await response.Content.ReadAsStringAsync();
57+
Assert.Equal("WebHook processed successfully! Foobar", body);
58+
}
59+
60+
[Fact]
61+
public async Task GenericWebHook_Post_AdminKey_Succeeds()
62+
{
63+
// Verify that sending the admin key bypasses WebHook auth
64+
string uri = "api/webhook-generic?code=t8laajal0a1ajkgzoqlfv5gxr4ebhqozebw4qzdy";
65+
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, uri);
66+
request.Content = new StringContent("{ 'a': 'Foobar' }");
67+
request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
68+
69+
HttpResponseMessage response = await this._fixture.Client.SendAsync(request);
70+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
71+
string body = await response.Content.ReadAsStringAsync();
72+
Assert.Equal("WebHook processed successfully! Foobar", body);
73+
}
74+
75+
public class TestFixture
76+
{
77+
public TestFixture()
78+
{
79+
HttpConfiguration config = new HttpConfiguration();
80+
81+
WebHostSettings settings = new WebHostSettings
82+
{
83+
IsSelfHost = true,
84+
ScriptPath = Path.Combine(Environment.CurrentDirectory, @"..\..\..\..\sample"),
85+
LogPath = Path.Combine(Path.GetTempPath(), @"Functions"),
86+
SecretsPath = Path.Combine(Environment.CurrentDirectory, @"..\..\..\..\src\WebJobs.Script.WebHost\App_Data\Secrets")
87+
};
88+
WebApiConfig.Register(config, settings);
89+
90+
HttpServer server = new HttpServer(config);
91+
this.Client = new HttpClient(server);
92+
this.Client.BaseAddress = new Uri("https://localhost/");
93+
}
94+
95+
public HttpClient Client { get; set; }
96+
}
97+
}
98+
}

test/WebJobs.Script.Tests/WebJobs.Script.Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,7 @@
183183
<Compile Include="FileTraceWriterTests.cs" />
184184
<Compile Include="FSharpEndToEndTests.cs" />
185185
<Compile Include="FunctionGeneratorTests.cs" />
186+
<Compile Include="SamplesEndToEndTests.cs" />
186187
<Compile Include="PhpEndToEndTests.cs" />
187188
<Compile Include="PowershellEndToEndTests.cs" />
188189
<Compile Include="PythonEndToEndTests.cs" />

0 commit comments

Comments
 (0)