Skip to content

Commit 463c9cf

Browse files
authored
Configuration based whitelisting of /admin API internal auth (#10487)
1 parent 5e3f9a7 commit 463c9cf

File tree

7 files changed

+140
-4
lines changed

7 files changed

+140
-4
lines changed

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
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;
1112
using Microsoft.Extensions.DependencyInjection;
1213

1314
namespace Microsoft.Azure.WebJobs.Script.WebHost.Security.Authorization.Policies
@@ -52,7 +53,8 @@ public static void AddScriptPolicies(this AuthorizationOptions options)
5253
return false;
5354
}
5455

55-
if (filterContext.HttpContext.Request.IsAppServiceInternalRequest())
56+
if (filterContext.HttpContext.Request.IsAppServiceInternalRequest() &&
57+
filterContext.HttpContext.Request.IsInternalAuthAllowed())
5658
{
5759
return true;
5860
}
@@ -74,7 +76,8 @@ public static void AddScriptPolicies(this AuthorizationOptions options)
7476
{
7577
if (c.Resource is AuthorizationFilterContext filterContext)
7678
{
77-
if (filterContext.HttpContext.Request.IsAppServiceInternalRequest())
79+
if (filterContext.HttpContext.Request.IsAppServiceInternalRequest() &&
80+
filterContext.HttpContext.Request.IsInternalAuthAllowed())
7881
{
7982
return true;
8083
}

src/WebJobs.Script/Config/FunctionsHostingConfigOptions.cs

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,16 @@
33

44
using System;
55
using System.Collections.Generic;
6+
using System.Linq;
7+
using Microsoft.AspNetCore.Http;
68
using Microsoft.Azure.WebJobs.Script.Workers.Rpc;
79

810
namespace Microsoft.Azure.WebJobs.Script.Config
911
{
1012
public class FunctionsHostingConfigOptions
1113
{
1214
private readonly Dictionary<string, string> _features;
15+
private PathString[] _allowedInternalAuthApis;
1316

1417
public FunctionsHostingConfigOptions()
1518
{
@@ -91,6 +94,24 @@ internal bool SwtIssuerEnabled
9194
}
9295
}
9396

97+
/// <summary>
98+
/// Gets or sets a string delimited by '|' that contains a list of admin APIs that are allowed to
99+
/// be invoked internally by platform components.
100+
/// </summary>
101+
internal string InternalAuthApisAllowList
102+
{
103+
get
104+
{
105+
return GetFeature(ScriptConstants.HostingConfigInternalAuthApisAllowList);
106+
}
107+
108+
set
109+
{
110+
_allowedInternalAuthApis = null;
111+
_features[ScriptConstants.HostingConfigInternalAuthApisAllowList] = value;
112+
}
113+
}
114+
94115
/// <summary>
95116
/// Gets a string delimited by '|' that contains the name of the apps with worker indexing disabled.
96117
/// </summary>
@@ -242,8 +263,34 @@ internal bool GetFeatureAsBooleanOrDefault(string name, bool defaultValue)
242263
{
243264
return parsedInt != 0;
244265
}
245-
246266
return defaultValue;
247267
}
268+
269+
internal bool CheckInternalAuthAllowList(HttpRequest httpRequest)
270+
{
271+
if (InternalAuthApisAllowList != null && _allowedInternalAuthApis == null)
272+
{
273+
// initialize our cached allow list on demand
274+
_allowedInternalAuthApis = InternalAuthApisAllowList.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries).Select(p => new PathString(p)).ToArray();
275+
}
276+
277+
if (_allowedInternalAuthApis != null)
278+
{
279+
// An allow list is configured, so we ensure that the current request
280+
// matches any of the allowed APIs.
281+
foreach (PathString ps in _allowedInternalAuthApis)
282+
{
283+
if (httpRequest.Path.StartsWithSegments(ps, StringComparison.OrdinalIgnoreCase))
284+
{
285+
return true;
286+
}
287+
}
288+
289+
return false;
290+
}
291+
292+
// no allow list configured
293+
return true;
294+
}
248295
}
249296
}

src/WebJobs.Script/Extensions/HttpRequestExtensions.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313
using Microsoft.AspNetCore.Http.Extensions;
1414
using Microsoft.AspNetCore.WebUtilities;
1515
using Microsoft.Azure.WebJobs.Extensions.Http;
16+
using Microsoft.Azure.WebJobs.Script.Config;
1617
using Microsoft.Extensions.DependencyInjection;
18+
using Microsoft.Extensions.Options;
1719
using Microsoft.Extensions.Primitives;
1820
using Newtonsoft.Json;
1921
using Newtonsoft.Json.Linq;
@@ -102,6 +104,13 @@ public static bool IsPlatformInternalRequest(this HttpRequest request, IEnvironm
102104
return string.Compare(value, "true", StringComparison.OrdinalIgnoreCase) == 0;
103105
}
104106

107+
public static bool IsInternalAuthAllowed(this HttpRequest httpRequest)
108+
{
109+
// Check to see if the specific API is allowed based on any configured allow list
110+
var options = httpRequest.HttpContext.RequestServices.GetService<IOptions<FunctionsHostingConfigOptions>>().Value;
111+
return options.CheckInternalAuthAllowList(httpRequest);
112+
}
113+
105114
private static IEnvironment GetEnvironment(HttpRequest request, IEnvironment environment = null)
106115
{
107116
return environment ?? request.HttpContext.RequestServices?.GetService<IEnvironment>() ?? SystemEnvironment.Instance;

src/WebJobs.Script/ScriptConstants.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ public static class ScriptConstants
143143
public const string FeatureFlagEnableLegacyDurableVersionCheck = "EnableLegacyDurableVersionCheck";
144144
public const string HostingConfigSwtAuthenticationEnabled = "SwtAuthenticationEnabled";
145145
public const string HostingConfigSwtIssuerEnabled = "SwtIssuerEnabled";
146+
public const string HostingConfigInternalAuthApisAllowList = "InternalAuthApisAllowList";
146147

147148
public const string SiteAzureFunctionsUriFormat = "https://{0}.azurewebsites.net/azurefunctions";
148149
public const string ScmSiteUriFormat = "https://{0}.scm.azurewebsites.net";

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -522,6 +522,33 @@ public async Task SyncTriggers_InternalAuth_Succeeds()
522522
}
523523
}
524524

525+
[Theory]
526+
[InlineData("", HttpStatusCode.Unauthorized)]
527+
[InlineData("|", HttpStatusCode.Unauthorized)]
528+
[InlineData("/admin/host/foo|/admin/host/bar", HttpStatusCode.Unauthorized)]
529+
[InlineData("/admin/host/status|/admin/host/synctriggers", HttpStatusCode.OK)]
530+
public async Task SyncTriggers_InternalAuth_AllowListSpecified_ReturnsExpectedResult(string allowList, HttpStatusCode expected)
531+
{
532+
var options = _fixture.Host.WebHostServices.GetService<IOptions<FunctionsHostingConfigOptions>>().Value;
533+
534+
try
535+
{
536+
options.InternalAuthApisAllowList = allowList;
537+
538+
using (var httpClient = _fixture.Host.CreateHttpClient())
539+
{
540+
string uri = "admin/host/synctriggers";
541+
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, uri);
542+
HttpResponseMessage response = await httpClient.SendAsync(request);
543+
Assert.Equal(expected, response.StatusCode);
544+
}
545+
}
546+
finally
547+
{
548+
options.InternalAuthApisAllowList = null;
549+
}
550+
}
551+
525552
[Fact]
526553
public async Task SyncTriggers_ExternalUnauthorized_ReturnsUnauthorized()
527554
{

test/WebJobs.Script.Tests/Configuration/FunctionsHostingConfigOptionsTest.cs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,10 @@ public void Property_Validation()
139139
(nameof(FunctionsHostingConfigOptions.ThrowOnMissingFunctionsWorkerRuntime), "THROW_ON_MISSING_FUNCTIONS_WORKER_RUNTIME=1", true),
140140
(nameof(FunctionsHostingConfigOptions.WorkerIndexingDisabledApps), "WORKER_INDEXING_DISABLED_APPS=teststring", "teststring"),
141141
(nameof(FunctionsHostingConfigOptions.WorkerIndexingEnabled), "WORKER_INDEXING_ENABLED=1", true),
142-
(nameof(FunctionsHostingConfigOptions.WorkerRuntimeStrictValidationEnabled), "WORKER_RUNTIME_STRICT_VALIDATION_ENABLED=1", true)
142+
(nameof(FunctionsHostingConfigOptions.WorkerRuntimeStrictValidationEnabled), "WORKER_RUNTIME_STRICT_VALIDATION_ENABLED=1", true),
143+
144+
(nameof(FunctionsHostingConfigOptions.InternalAuthApisAllowList), "InternalAuthApisAllowList=|", "|"),
145+
(nameof(FunctionsHostingConfigOptions.InternalAuthApisAllowList), "InternalAuthApisAllowList=/admin/host/foo|/admin/host/bar", "/admin/host/foo|/admin/host/bar")
143146
};
144147

145148
// use reflection to ensure that we have a test that uses every value exposed on FunctionsHostingConfigOptions
@@ -281,6 +284,20 @@ public void SwtIssuerEnabled_ReturnsExpectedValue()
281284
Assert.False(options.SwtIssuerEnabled);
282285
}
283286

287+
[Fact]
288+
public void InternalAuthApisAllowList_ReturnsExpectedValue()
289+
{
290+
FunctionsHostingConfigOptions options = new FunctionsHostingConfigOptions();
291+
292+
Assert.Null(options.InternalAuthApisAllowList);
293+
294+
options.InternalAuthApisAllowList = string.Empty;
295+
Assert.Equal(string.Empty, options.InternalAuthApisAllowList);
296+
297+
options.InternalAuthApisAllowList = "/admin/host/synctriggers|/admin/host/status";
298+
Assert.Equal("/admin/host/synctriggers|/admin/host/status", options.InternalAuthApisAllowList);
299+
}
300+
284301
internal static IHostBuilder GetScriptHostBuilder(string fileName, string fileContent)
285302
{
286303
if (!string.IsNullOrEmpty(fileContent))

test/WebJobs.Script.Tests/Extensions/HttpRequestExtensionsTest.cs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@
1010
using System.Threading.Tasks;
1111
using Microsoft.AspNetCore.Http;
1212
using Microsoft.AspNetCore.WebUtilities;
13+
using Microsoft.Azure.WebJobs.Script.Config;
1314
using Microsoft.Azure.WebJobs.Script.Extensions;
1415
using Microsoft.Azure.WebJobs.Script.Tests.HttpWorker;
16+
using Microsoft.Extensions.Options;
1517
using Microsoft.WebJobs.Script.Tests;
1618
using Moq;
1719
using Xunit;
@@ -68,6 +70,36 @@ public void IsPlatformInternalRequest_ReturnsExpectedResult()
6870
}
6971
}
7072

73+
[Theory]
74+
[InlineData("/admin/host/drain", null, true)] // no allow list
75+
[InlineData("/admin/host/drain", "", false)] // empty allow list
76+
[InlineData("/admin/host/drain", "|", false)] // empty allow list
77+
[InlineData("/admin/host/drain", "/admin/host/foo|/admin/host/bar", false)] // target not in allow list
78+
[InlineData("/admin/host/synctriggers", "/admin/host/status|/admin/host/synctriggers", true)] // target in allow list
79+
[InlineData("/runtime/webhooks/foo/", "/admin/host/status|/admin/host/synctriggers|/runtime/webhooks", true)] // target in allow list
80+
[InlineData("/runtime/webhooks/foo/bar?foo=1&bar=2", "/admin/host/status|/admin/host/synctriggers|/runtime/webhooks", true)] // target in allow list
81+
[InlineData("/runtime/webhooks/foo/bar/", "/admin/host/status|/admin/host/synctriggers|/runtime/webhooks", true)] // target in allow list
82+
[InlineData("/runtime/webhooks/foo/bar/", "/admin/host/status|/admin/host/synctriggers", false)] // target not in allow list
83+
public void IsInternalAuthAllowed_ReturnsExpectedResult(string targetPath, string allowList, bool expected)
84+
{
85+
var options = new FunctionsHostingConfigOptions();
86+
87+
if (allowList != null)
88+
{
89+
options.InternalAuthApisAllowList = allowList;
90+
}
91+
92+
var request = HttpTestHelpers.CreateHttpRequest("GET", $"http://host{targetPath}");
93+
94+
var environment = SystemEnvironment.Instance;
95+
var servicesMock = new Mock<IServiceProvider>();
96+
servicesMock.Setup(s => s.GetService(typeof(IEnvironment))).Returns(environment);
97+
servicesMock.Setup(s => s.GetService(typeof(IOptions<FunctionsHostingConfigOptions>))).Returns(new OptionsWrapper<FunctionsHostingConfigOptions>(options));
98+
request.HttpContext.RequestServices = servicesMock.Object;
99+
100+
Assert.Equal(expected, request.IsInternalAuthAllowed());
101+
}
102+
71103
[Fact]
72104
public void IsAppServiceInternalRequest_ReturnsExpectedResult()
73105
{

0 commit comments

Comments
 (0)