Skip to content

Commit 303117f

Browse files
authored
Adding support for admin API isolation (#8457)
1 parent 77cc8a7 commit 303117f

File tree

14 files changed

+328
-20
lines changed

14 files changed

+328
-20
lines changed
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": "req",
6+
"direction": "in",
7+
"methods": [ "get" ],
8+
"authLevel": "admin"
9+
},
10+
{
11+
"type": "http",
12+
"name": "$return",
13+
"direction": "out"
14+
}
15+
]
16+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using System.Net;
2+
using Microsoft.AspNetCore.Mvc;
3+
using Microsoft.Extensions.Primitives;
4+
5+
public static IActionResult Run(HttpRequest req, TraceWriter log)
6+
{
7+
log.Info("C# HTTP trigger function processed a request.");
8+
9+
if (req.Query.TryGetValue("name", out StringValues value))
10+
{
11+
return new OkObjectResult($"Hello {value.ToString()}");
12+
}
13+
14+
return new BadRequestObjectResult("Please pass a name on the query string or in the request body");
15+
}

src/WebJobs.Script.WebHost/Middleware/VirtualFileSystemMiddleware.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,11 @@ private async Task<bool> AuthenticateAndAuthorize(HttpContext context)
104104
var authorizationPolicyProvider = context.RequestServices.GetRequiredService<IAuthorizationPolicyProvider>();
105105
var policyEvaluator = context.RequestServices.GetRequiredService<IPolicyEvaluator>();
106106

107+
if (!AuthorizationOptionsExtensions.CheckPlatformInternal(context, allowAppServiceInternal: false))
108+
{
109+
return false;
110+
}
111+
107112
var policy = await authorizationPolicyProvider.GetPolicyAsync(PolicyNames.AdminAuthLevel);
108113
var authenticateResult = await policyEvaluator.AuthenticateAsync(policy, context);
109114

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

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
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.Collections.Generic;
54
using Microsoft.AspNetCore.Authentication.JwtBearer;
65
using Microsoft.AspNetCore.Authorization;
76
using Microsoft.AspNetCore.Http;
87
using Microsoft.AspNetCore.Mvc.Filters;
9-
using Microsoft.AspNetCore.Routing;
108
using Microsoft.Azure.WebJobs.Extensions.Http;
119
using Microsoft.Azure.WebJobs.Script.Extensions;
1210
using Microsoft.Azure.WebJobs.Script.WebHost.Authentication;
@@ -22,6 +20,18 @@ public static void AddScriptPolicies(this AuthorizationOptions options)
2220
{
2321
p.AddScriptAuthenticationSchemes();
2422
p.AddRequirements(new AuthLevelRequirement(AuthorizationLevel.Admin));
23+
p.RequireAssertion(c =>
24+
{
25+
if (c.Resource is AuthorizationFilterContext filterContext)
26+
{
27+
if (!CheckPlatformInternal(filterContext.HttpContext, allowAppServiceInternal: false))
28+
{
29+
return false;
30+
}
31+
}
32+
33+
return true;
34+
});
2535
});
2636

2737
options.AddPolicy(PolicyNames.SystemAuthLevel, p =>
@@ -37,6 +47,11 @@ public static void AddScriptPolicies(this AuthorizationOptions options)
3747
{
3848
if (c.Resource is AuthorizationFilterContext filterContext)
3949
{
50+
if (!CheckPlatformInternal(filterContext.HttpContext, allowAppServiceInternal: true))
51+
{
52+
return false;
53+
}
54+
4055
if (filterContext.HttpContext.Request.IsAppServiceInternalRequest())
4156
{
4257
return true;
@@ -96,5 +111,20 @@ private static void AddScriptAuthenticationSchemes(this AuthorizationPolicyBuild
96111
builder.AuthenticationSchemes.Add(AuthLevelAuthenticationDefaults.AuthenticationScheme);
97112
builder.AuthenticationSchemes.Add(JwtBearerDefaults.AuthenticationScheme);
98113
}
114+
115+
internal static bool CheckPlatformInternal(HttpContext httpContext, bool allowAppServiceInternal)
116+
{
117+
// when AdminIsolation is enabled, verify the request is platform internal
118+
var environment = httpContext.RequestServices.GetRequiredService<IEnvironment>();
119+
if (environment.IsAdminIsolationEnabled() &&
120+
!(httpContext.Request.IsPlatformInternalRequest(environment) || (allowAppServiceInternal && httpContext.Request.IsAppServiceInternalRequest())))
121+
{
122+
// request must either be granted PlatformInternal by FrontEnd, or must be an internal
123+
// request that has bypassed FrontEnd
124+
return false;
125+
}
126+
127+
return true;
128+
}
99129
}
100130
}

src/WebJobs.Script/Environment/EnvironmentExtensions.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@ public static bool IsRuntimeScaleMonitoringEnabled(this IEnvironment environment
4646
return environment.GetEnvironmentVariable(FunctionsRuntimeScaleMonitoringEnabled) == "1";
4747
}
4848

49+
public static bool IsAdminIsolationEnabled(this IEnvironment environment)
50+
{
51+
return environment.GetEnvironmentVariable(FunctionsAdminIsolationEnabled) == "1";
52+
}
53+
4954
public static bool IsEasyAuthEnabled(this IEnvironment environment)
5055
{
5156
bool.TryParse(environment.GetEnvironmentVariable(EasyAuthEnabled), out bool isEasyAuthEnabled);

src/WebJobs.Script/Environment/EnvironmentSettingNames.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ public static class EnvironmentSettingNames
5656
public const string AzureFilesContentShare = "WEBSITE_CONTENTSHARE";
5757
public const string AzureWebsiteRuntimeSiteName = "WEBSITE_DEPLOYMENT_ID";
5858
public const string FunctionsRuntimeScaleMonitoringEnabled = "FUNCTIONS_RUNTIME_SCALE_MONITORING_ENABLED";
59+
public const string FunctionsAdminIsolationEnabled = "FUNCTIONS_ADMIN_ISOLATION_ENABLED";
5960
public const string AzureWebsiteStartupContextCache = "WEBSITE_FUNCTIONS_STARTUPCONTEXT_CACHE";
6061
public const string AzureWebJobsFeatureFlags = "AzureWebJobsFeatureFlags";
6162
public const string CloudName = "WEBSITE_CLOUD_NAME";

src/WebJobs.Script/Extensions/HttpRequestExtensions.cs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
using Microsoft.AspNetCore.Http.Extensions;
1414
using Microsoft.AspNetCore.WebUtilities;
1515
using Microsoft.Azure.WebJobs.Extensions.Http;
16+
using Microsoft.CodeAnalysis.CSharp.Syntax;
17+
using Microsoft.Extensions.DependencyInjection;
1618
using Microsoft.Extensions.Primitives;
1719
using Newtonsoft.Json;
1820
using Newtonsoft.Json.Linq;
@@ -76,7 +78,7 @@ public static TValue GetItemOrDefault<TValue>(this HttpRequest request, string k
7678

7779
public static bool IsAppServiceInternalRequest(this HttpRequest request, IEnvironment environment = null)
7880
{
79-
environment = environment ?? SystemEnvironment.Instance;
81+
environment = GetEnvironment(request, environment);
8082
if (!environment.IsAppService())
8183
{
8284
return false;
@@ -88,6 +90,24 @@ public static bool IsAppServiceInternalRequest(this HttpRequest request, IEnviro
8890
return !request.Headers.Keys.Contains(ScriptConstants.AntaresLogIdHeaderName);
8991
}
9092

93+
public static bool IsPlatformInternalRequest(this HttpRequest request, IEnvironment environment = null)
94+
{
95+
environment = GetEnvironment(request, environment);
96+
if (!environment.IsAppService())
97+
{
98+
return false;
99+
}
100+
101+
var header = request.Headers[ScriptConstants.AntaresPlatformInternal];
102+
string value = header.FirstOrDefault();
103+
return string.Compare(value, "true", StringComparison.OrdinalIgnoreCase) == 0;
104+
}
105+
106+
private static IEnvironment GetEnvironment(HttpRequest request, IEnvironment environment = null)
107+
{
108+
return environment ?? request.HttpContext.RequestServices?.GetService<IEnvironment>() ?? SystemEnvironment.Instance;
109+
}
110+
91111
public static bool IsColdStart(this HttpRequest request)
92112
{
93113
return !string.IsNullOrEmpty(request.GetHeaderValueOrDefault(ScriptConstants.AntaresColdStartHeaderName));

src/WebJobs.Script/ScriptConstants.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ public static class ScriptConstants
9494
public const string AntaresColdStartHeaderName = "X-MS-COLDSTART";
9595
public const string SiteTokenHeaderName = "x-ms-site-restricted-token";
9696
public const string EasyAuthIdentityHeader = "x-ms-client-principal";
97+
public const string AntaresPlatformInternal = "x-ms-platform-internal";
9798
public const string AzureVersionHeader = "x-ms-version";
9899
public const string XIdentityHeader = "X-IDENTITY-HEADER";
99100
public const string DynamicSku = "Dynamic";

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,14 @@ public TestFunctionHost(string scriptPath, string logPath,
171171
HttpClient = _testServer.CreateClient();
172172
HttpClient.Timeout = TimeSpan.FromMinutes(5);
173173

174+
var environment = _testServer.Services.GetService<IEnvironment>();
175+
if (environment.IsAppService())
176+
{
177+
// host is configured to simulate an AppService environment
178+
// all normal requests will go through the Antares FrontEnd and receive this header
179+
HttpClient.DefaultRequestHeaders.Add(ScriptConstants.AntaresLogIdHeaderName, "xyz");
180+
}
181+
174182
var manager = _testServer.Host.Services.GetService<IScriptHostManager>();
175183
_hostService = manager as WebJobsScriptHostService;
176184

@@ -208,6 +216,17 @@ public TestFunctionHost(string scriptPath, string logPath,
208216

209217
public HttpClient HttpClient { get; private set; }
210218

219+
/// <summary>
220+
/// Create a new HttpClient without default test configuration.
221+
/// </summary>
222+
public HttpClient CreateHttpClient()
223+
{
224+
var httpClient = _testServer.CreateClient();
225+
httpClient.Timeout = TimeSpan.FromMinutes(5);
226+
227+
return httpClient;
228+
}
229+
211230
public async Task<string> GetMasterKeyAsync()
212231
{
213232
if (!SecretManagerProvider.SecretsEnabled)

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
using Microsoft.Azure.WebJobs.Script.Diagnostics;
1515
using Microsoft.Azure.WebJobs.Script.ExtensionBundle;
1616
using Microsoft.Azure.WebJobs.Script.Models;
17+
using Microsoft.Azure.WebJobs.Script.WebHost.Authentication;
1718
using Microsoft.Azure.WebJobs.Script.WebHost.Diagnostics;
1819
using Microsoft.Azure.WebJobs.Script.WebHost.Management;
1920
using Microsoft.Azure.WebJobs.Script.Workers.Rpc;
@@ -318,6 +319,12 @@ public void AssertScriptHostErrors()
318319
Assert.True(errors.Count > 0);
319320
}
320321

322+
public async Task AddMasterKey(HttpRequestMessage request)
323+
{
324+
var masterKey = await Host.GetMasterKeyAsync();
325+
request.Headers.Add(AuthenticationLevelHandler.FunctionsKeyHeaderName, masterKey);
326+
}
327+
321328
private class TestExtensionBundleManager : IExtensionBundleManager
322329
{
323330
public Task<string> GetExtensionBundleBinPathAsync() => Task.FromResult<string>(null);

0 commit comments

Comments
 (0)