Skip to content

Commit 67865b3

Browse files
mathewcfabiocav
authored andcommitted
Supporting configuration for extension webhook Authorization level (#11383)
1 parent a9abdf3 commit 67865b3

File tree

13 files changed

+295
-35
lines changed

13 files changed

+295
-35
lines changed

sample/CSharp/host.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@
2424
"queues": {
2525
"visibilityTimeout": "00:00:10",
2626
"maxDequeueCount": 3
27-
}
27+
},
28+
"test2": {
29+
"system": {
30+
"webhookAuthorizationLevel": "anonymous"
31+
}
32+
}
2833
}
2934
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
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.Text.Json;
6+
using System.Text.Json.Serialization;
7+
using Microsoft.Azure.WebJobs.Extensions.Http;
8+
using Microsoft.Azure.WebJobs.Hosting;
9+
10+
namespace Microsoft.Azure.WebJobs.Script.WebHost.Configuration;
11+
12+
/// <summary>
13+
/// Represents the system options for an individual extension.
14+
/// </summary>
15+
public sealed class ExtensionSystemOptions : IOptionsFormatter
16+
{
17+
private AuthorizationLevel _webhookAuthorizationLevel = AuthorizationLevel.System;
18+
19+
/// <summary>
20+
/// Gets or sets the name of the extension these options apply to.
21+
/// </summary>
22+
public string ExtensionName { get; set; }
23+
24+
/// <summary>
25+
/// Gets or sets the default authorization level for the extension. Only applies to WebHook extensions.
26+
/// </summary>
27+
[JsonConverter(typeof(JsonStringEnumConverter<AuthorizationLevel>))]
28+
public AuthorizationLevel WebhookAuthorizationLevel
29+
{
30+
get => _webhookAuthorizationLevel;
31+
set
32+
{
33+
if (value != AuthorizationLevel.System && value != AuthorizationLevel.Anonymous)
34+
{
35+
throw new ArgumentOutOfRangeException(nameof(value), $"Invalid AuthorizationLevel: {value}");
36+
}
37+
_webhookAuthorizationLevel = value;
38+
}
39+
}
40+
41+
public string Format()
42+
{
43+
return JsonSerializer.Serialize(this, ExtensionSystemOptionsJsonSerializerContext.Default.ExtensionSystemOptions);
44+
}
45+
}
46+
47+
[JsonSourceGenerationOptions(WriteIndented = true, PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
48+
[JsonSerializable(typeof(ExtensionSystemOptions))]
49+
internal partial class ExtensionSystemOptionsJsonSerializerContext : JsonSerializerContext;
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
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 Microsoft.Azure.WebJobs.Script.Configuration;
5+
using Microsoft.Extensions.Configuration;
6+
using Microsoft.Extensions.Options;
7+
8+
namespace Microsoft.Azure.WebJobs.Script.WebHost.Configuration;
9+
10+
public sealed class ExtensionSystemOptionsSetup : IConfigureNamedOptions<ExtensionSystemOptions>
11+
{
12+
private const string SystemSectionName = "system";
13+
private readonly IConfiguration _configuration;
14+
15+
public ExtensionSystemOptionsSetup(IConfiguration configuration)
16+
{
17+
_configuration = configuration;
18+
}
19+
20+
public void Configure(string name, ExtensionSystemOptions options)
21+
{
22+
if (string.IsNullOrEmpty(name))
23+
{
24+
return;
25+
}
26+
27+
string path = ConfigurationPath.Combine(ConfigurationSectionNames.JobHost, ConfigurationSectionNames.Extensions, name, SystemSectionName);
28+
IConfigurationSection section = _configuration.GetSection(path);
29+
section?.Bind(options);
30+
options.ExtensionName = name;
31+
}
32+
33+
public void Configure(ExtensionSystemOptions options)
34+
{
35+
Configure(Options.DefaultName, options);
36+
}
37+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -518,7 +518,7 @@ public async Task<IActionResult> SetState([FromBody] string state)
518518
}
519519

520520
[AcceptVerbs("GET", "PUT", "POST", "DELETE", "OPTIONS")]
521-
[Authorize(Policy = PolicyNames.SystemKeyAuthLevel)]
521+
[Authorize(Policy = PolicyNames.ExtensionWebhookInvoke)]
522522
[Route("runtime/webhooks/{extensionName}/{*extra}")]
523523
[RequiresRunningHost]
524524
public async Task<IActionResult> ExtensionWebHookHandler(string extensionName, CancellationToken token, [FromServices] IScriptWebHookProvider provider)

src/WebJobs.Script.WebHost/Security/Authorization/Policies/AuthorizationOptionsExtensions.cs

Lines changed: 16 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@
88
using Microsoft.Azure.WebJobs.Extensions.Http;
99
using Microsoft.Azure.WebJobs.Script.Extensions;
1010
using Microsoft.Azure.WebJobs.Script.WebHost.Authentication;
11-
using Microsoft.Azure.WebJobs.Script.WebHost.Extensions;
11+
using Microsoft.Azure.WebJobs.Script.WebHost.Configuration;
1212
using Microsoft.Extensions.DependencyInjection;
13+
using Microsoft.Extensions.Options;
1314

1415
namespace Microsoft.Azure.WebJobs.Script.WebHost.Security.Authorization.Policies
1516
{
@@ -35,34 +36,29 @@ public static void AddScriptPolicies(this AuthorizationOptions options)
3536
});
3637
});
3738

38-
options.AddPolicy(PolicyNames.SystemAuthLevel, p =>
39-
{
40-
p.AddScriptAuthenticationSchemes();
41-
p.AddRequirements(new AuthLevelRequirement(AuthorizationLevel.System));
42-
});
43-
44-
options.AddPolicy(PolicyNames.SystemKeyAuthLevel, p =>
39+
options.AddPolicy(PolicyNames.ExtensionWebhookInvoke, p =>
4540
{
4641
p.AddScriptAuthenticationSchemes();
4742
p.RequireAssertion(c =>
4843
{
4944
if (c.Resource is AuthorizationFilterContext filterContext)
5045
{
51-
string keyName = null;
52-
object keyNameObject = filterContext.RouteData.Values["extensionName"];
53-
if (keyNameObject != null)
54-
{
55-
keyName = DefaultScriptWebHookProvider.GetKeyName(keyNameObject.ToString());
56-
}
57-
else
46+
string extensionName = filterContext.RouteData.Values["extensionName"]?.ToString();
47+
48+
// First check to see if anonymous access has been configured for the webhook.
49+
// E.g. this might be configured for a Webhook extension in an app where App Service
50+
// authentication is being used.
51+
var snapshot = filterContext.HttpContext.RequestServices.GetRequiredService<IOptionsSnapshot<ExtensionSystemOptions>>();
52+
var extensionSystemOptions = snapshot.Get(extensionName);
53+
if (!string.IsNullOrEmpty(extensionName) && extensionSystemOptions?.WebhookAuthorizationLevel == AuthorizationLevel.Anonymous)
5854
{
59-
keyNameObject = filterContext.RouteData.Values["keyName"];
60-
if (keyNameObject != null)
61-
{
62-
keyName = keyNameObject.ToString();
63-
}
55+
return true;
6456
}
6557

58+
string keyName = !string.IsNullOrEmpty(extensionName)
59+
? DefaultScriptWebHookProvider.GetKeyName(extensionName)
60+
: filterContext.RouteData.Values["keyName"]?.ToString();
61+
6662
if (!string.IsNullOrEmpty(keyName) && AuthUtility.PrincipalHasAuthLevelClaim(filterContext.HttpContext.User, AuthorizationLevel.System, keyName))
6763
{
6864
return true;
Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,11 @@
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;
5-
using System.Collections.Generic;
6-
using System.Linq;
7-
using System.Threading.Tasks;
8-
94
namespace Microsoft.Azure.WebJobs.Script.WebHost.Security.Authorization.Policies
105
{
116
public static class PolicyNames
127
{
138
public const string AdminAuthLevel = "AuthLevelAdmin";
14-
public const string SystemAuthLevel = "AuthLevelSystem";
15-
public const string FunctionAuthLevel = "AuthLevelFunction";
16-
public const string SystemKeyAuthLevel = "AuthLevelSystemKey";
9+
public const string ExtensionWebhookInvoke = "Extension.Webhook.Invoke";
1710
}
1811
}

src/WebJobs.Script.WebHost/WebScriptHostBuilderExtension.cs

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

44
using System;
@@ -58,6 +58,7 @@ public static IHostBuilder AddWebScriptHost(this IHostBuilder builder, IServiceP
5858
services.ConfigureOptions<AppServiceOptionsSetup>();
5959
services.ConfigureOptions<HostEasyAuthOptionsSetup>();
6060
services.ConfigureOptions<PrimaryHostCoordinatorOptionsSetup>();
61+
services.ConfigureOptions<ExtensionSystemOptionsSetup>();
6162
})
6263
.AddScriptHost(webHostOptions, configLoggerFactory, metricsLogger, webJobsBuilder =>
6364
{

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

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,38 @@ public async Task InvokeFunction_AdminInvokeApi_Succeeds()
161161
}
162162

163163
[Fact]
164+
[Trait(TestTraits.Group, TestTraits.WebhookTests)]
165+
public async Task ExtensionWebHook_AuthorizationLevelOverride_Succeeds()
166+
{
167+
// configure a mock webhook handler for the "test2" extension
168+
// for which we've overridden the auth level to "Anonymous"
169+
Mock<IAsyncConverter<HttpRequestMessage, HttpResponseMessage>> mockHandler = new Mock<IAsyncConverter<HttpRequestMessage, HttpResponseMessage>>(MockBehavior.Strict);
170+
mockHandler.Setup(p => p.ConvertAsync(It.IsAny<HttpRequestMessage>(), It.IsAny<CancellationToken>()))
171+
.ReturnsAsync(() => new HttpResponseMessage(HttpStatusCode.OK));
172+
var handler = mockHandler.Object;
173+
_fixture.MockWebHookProvider.Setup(p => p.TryGetHandler("test2", out handler)).Returns(true);
174+
175+
// Verify that if the valid system key is specified, the request succeeds
176+
string uri = "runtime/webhooks/test2?code=SystemValue4";
177+
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, uri);
178+
HttpResponseMessage response = await _fixture.Host.HttpClient.SendAsync(request);
179+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
180+
181+
// Verify that if an invalid system key is specified, the request also succeeds, since auth is not required
182+
uri = "runtime/webhooks/test2?code=invalid";
183+
request = new HttpRequestMessage(HttpMethod.Get, uri);
184+
response = await _fixture.Host.HttpClient.SendAsync(request);
185+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
186+
187+
// No key provided, the request succeeds
188+
uri = "runtime/webhooks/test2";
189+
request = new HttpRequestMessage(HttpMethod.Get, uri);
190+
response = await _fixture.Host.HttpClient.SendAsync(request);
191+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
192+
}
193+
194+
[Fact]
195+
[Trait(TestTraits.Group, TestTraits.WebhookTests)]
164196
public async Task ExtensionWebHook_Succeeds()
165197
{
166198
// configure a mock webhook handler for the "test" extension

test/WebJobs.Script.Tests.Shared/TestSecretManager.cs

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

44
using System;
@@ -87,6 +87,7 @@ public void Reset()
8787
{ "SystemKey1", "SystemValue1" },
8888
{ "SystemKey2", "SystemValue2" },
8989
{ "Test_Extension", "SystemValue3" },
90+
{ "Test2_Extension", "SystemValue4" }
9091
};
9192
}
9293

test/WebJobs.Script.Tests.Shared/TestTraits.cs

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

44
namespace Microsoft.WebJobs.Script.Tests
@@ -63,5 +63,7 @@ public static class TestTraits
6363
public const string HostMetricsTests = "HostMetricsTests";
6464

6565
public const string HISSecretsTests = "HISSecretsTests";
66+
67+
public const string WebhookTests = "WebhookTests";
6668
}
6769
}

0 commit comments

Comments
 (0)