Skip to content

Commit aea2d2a

Browse files
authored
Adding support for admin API isolation (#8462)
1 parent 1d791dc commit aea2d2a

File tree

15 files changed

+343
-25
lines changed

15 files changed

+343
-25
lines changed

WebJobs.Script.sln

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11

22
Microsoft Visual Studio Solution File, Format Version 12.00
3-
# Visual Studio Version 16
4-
VisualStudioVersion = 16.0.29409.12
3+
# Visual Studio Version 17
4+
VisualStudioVersion = 17.2.32505.173
55
MinimumVisualStudioVersion = 15.0.0.0
66
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{16351B76-87CA-4A8C-80A1-3DD83A0C4AA6}"
77
EndProject
@@ -304,8 +304,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Python", "Python", "{0AE3CE
304304
EndProject
305305
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "HttpTrigger", "HttpTrigger", "{BA45A727-34B7-484F-9B93-B1755AF09A2A}"
306306
ProjectSection(SolutionItems) = preProject
307-
sample\Python\HttpTrigger\__init__.py = sample\Python\HttpTrigger\__init__.py
308307
sample\Python\HttpTrigger\function.json = sample\Python\HttpTrigger\function.json
308+
sample\Python\HttpTrigger\__init__.py = sample\Python\HttpTrigger\__init__.py
309309
EndProjectSection
310310
EndProject
311311
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebJobs.Script.Abstractions", "src\WebJobs.Script.Abstractions\WebJobs.Script.Abstractions.csproj", "{9A522D9D-2D86-4572-B7D1-ECBFBFAF312C}"
@@ -360,12 +360,13 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "HttpTrigger-LongRun", "Http
360360
sample\NodeDrain\HttpTrigger-LongRun\index.js = sample\NodeDrain\HttpTrigger-LongRun\index.js
361361
EndProjectSection
362362
EndProject
363+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "HttpTrigger-AdminLevel", "HttpTrigger-AdminLevel", "{7FDE25BC-43C0-4902-AB9B-8B09123716BA}"
364+
ProjectSection(SolutionItems) = preProject
365+
sample\CSharp\HttpTrigger-AdminLevel\function.json = sample\CSharp\HttpTrigger-AdminLevel\function.json
366+
sample\CSharp\HttpTrigger-AdminLevel\run.csx = sample\CSharp\HttpTrigger-AdminLevel\run.csx
367+
EndProjectSection
368+
EndProject
363369
Global
364-
GlobalSection(SharedMSBuildProjectFiles) = preSolution
365-
test\WebJobs.Script.Tests.Shared\WebJobs.Script.Tests.Shared.projitems*{35c9ccb7-d8b6-4161-bb0d-bcfa7c6dcffb}*SharedItemsImports = 13
366-
test\WebJobs.Script.Tests.Shared\WebJobs.Script.Tests.Shared.projitems*{3ba93614-3a4a-49b0-bfbc-7831e2c02bb7}*SharedItemsImports = 5
367-
test\WebJobs.Script.Tests.Shared\WebJobs.Script.Tests.Shared.projitems*{edddaed1-0e37-4ed7-a595-63f686dee90a}*SharedItemsImports = 5
368-
EndGlobalSection
369370
GlobalSection(SolutionConfigurationPlatforms) = preSolution
370371
Debug|Any CPU = Debug|Any CPU
371372
Release|Any CPU = Release|Any CPU
@@ -471,8 +472,14 @@ Global
471472
{09D16953-A048-4E6B-B366-1E0D7E5EF86E} = {AFB0F5F7-A612-4F4A-94DD-8B69CABF7970}
472473
{6FE94892-4E58-403D-BA32-4A35C0EE1E46} = {FF9C0818-30D3-437A-A62D-7A61CA44F459}
473474
{1EE80AFE-8B64-4670-9462-3DEA692D4457} = {6FE94892-4E58-403D-BA32-4A35C0EE1E46}
475+
{7FDE25BC-43C0-4902-AB9B-8B09123716BA} = {34506711-9D66-41EF-BBA1-9A9DC1140209}
474476
EndGlobalSection
475477
GlobalSection(ExtensibilityGlobals) = postSolution
476478
SolutionGuid = {85400884-5FFD-4C27-A571-58CB3C8CAAC5}
477479
EndGlobalSection
480+
GlobalSection(SharedMSBuildProjectFiles) = preSolution
481+
test\WebJobs.Script.Tests.Shared\WebJobs.Script.Tests.Shared.projitems*{35c9ccb7-d8b6-4161-bb0d-bcfa7c6dcffb}*SharedItemsImports = 13
482+
test\WebJobs.Script.Tests.Shared\WebJobs.Script.Tests.Shared.projitems*{3ba93614-3a4a-49b0-bfbc-7831e2c02bb7}*SharedItemsImports = 5
483+
test\WebJobs.Script.Tests.Shared\WebJobs.Script.Tests.Shared.projitems*{edddaed1-0e37-4ed7-a595-63f686dee90a}*SharedItemsImports = 5
484+
EndGlobalSection
478485
EndGlobal
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
@@ -103,6 +103,11 @@ private async Task<bool> AuthenticateAndAuthorize(HttpContext context)
103103
var authorizationPolicyProvider = context.RequestServices.GetRequiredService<IAuthorizationPolicyProvider>();
104104
var policyEvaluator = context.RequestServices.GetRequiredService<IPolicyEvaluator>();
105105

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

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

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,18 @@ public static void AddScriptPolicies(this AuthorizationOptions options)
2222
{
2323
p.AddScriptAuthenticationSchemes();
2424
p.AddRequirements(new AuthLevelRequirement(AuthorizationLevel.Admin));
25+
p.RequireAssertion(c =>
26+
{
27+
if (c.Resource is AuthorizationFilterContext filterContext)
28+
{
29+
if (!CheckPlatformInternal(filterContext.HttpContext, allowAppServiceInternal: false))
30+
{
31+
return false;
32+
}
33+
}
34+
35+
return true;
36+
});
2537
});
2638

2739
options.AddPolicy(PolicyNames.SystemAuthLevel, p =>
@@ -37,6 +49,11 @@ public static void AddScriptPolicies(this AuthorizationOptions options)
3749
{
3850
if (c.Resource is AuthorizationFilterContext filterContext)
3951
{
52+
if (!CheckPlatformInternal(filterContext.HttpContext, allowAppServiceInternal: true))
53+
{
54+
return false;
55+
}
56+
4057
if (filterContext.HttpContext.Request.IsAppServiceInternalRequest())
4158
{
4259
return true;
@@ -96,5 +113,20 @@ private static void AddScriptAuthenticationSchemes(this AuthorizationPolicyBuild
96113
builder.AuthenticationSchemes.Add(AuthLevelAuthenticationDefaults.AuthenticationScheme);
97114
builder.AuthenticationSchemes.Add(JwtBearerDefaults.AuthenticationScheme);
98115
}
116+
117+
internal static bool CheckPlatformInternal(HttpContext httpContext, bool allowAppServiceInternal)
118+
{
119+
// when AdminIsolation is enabled, verify the request is platform internal
120+
var environment = httpContext.RequestServices.GetRequiredService<IEnvironment>();
121+
if (environment.IsAdminIsolationEnabled() &&
122+
!(httpContext.Request.IsPlatformInternalRequest(environment) || (allowAppServiceInternal && httpContext.Request.IsAppServiceInternalRequest())))
123+
{
124+
// request must either be granted PlatformInternal by FrontEnd, or must be an internal
125+
// request that has bypassed FrontEnd
126+
return false;
127+
}
128+
129+
return true;
130+
}
99131
}
100132
}

src/WebJobs.Script/Environment/EnvironmentExtensions.cs

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

48+
public static bool IsAdminIsolationEnabled(this IEnvironment environment)
49+
{
50+
return environment.GetEnvironmentVariable(FunctionsAdminIsolationEnabled) == "1";
51+
}
52+
4853
public static bool IsEasyAuthEnabled(this IEnvironment environment)
4954
{
5055
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
@@ -52,6 +52,7 @@ public static class EnvironmentSettingNames
5252
public const string AzureFilesContentShare = "WEBSITE_CONTENTSHARE";
5353
public const string AzureWebsiteRuntimeSiteName = "WEBSITE_DEPLOYMENT_ID";
5454
public const string FunctionsRuntimeScaleMonitoringEnabled = "FUNCTIONS_RUNTIME_SCALE_MONITORING_ENABLED";
55+
public const string FunctionsAdminIsolationEnabled = "FUNCTIONS_ADMIN_ISOLATION_ENABLED";
5556
public const string AzureWebsiteStartupContextCache = "WEBSITE_FUNCTIONS_STARTUPCONTEXT_CACHE";
5657
public const string AzureWebJobsFeatureFlags = "AzureWebJobsFeatureFlags";
5758
public const string CloudName = "WEBSITE_CLOUD_NAME";

src/WebJobs.Script/Extensions/HttpRequestExtensions.cs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
using Microsoft.AspNetCore.Http.Extensions;
1414
using Microsoft.AspNetCore.WebUtilities;
1515
using Microsoft.Azure.WebJobs.Extensions.Http;
16+
using Microsoft.Extensions.DependencyInjection;
1617
using Microsoft.Extensions.Primitives;
1718
using Newtonsoft.Json;
1819
using Newtonsoft.Json.Linq;
@@ -78,7 +79,7 @@ public static TValue GetItemOrDefault<TValue>(this HttpRequest request, string k
7879

7980
public static bool IsAppServiceInternalRequest(this HttpRequest request, IEnvironment environment = null)
8081
{
81-
environment = environment ?? SystemEnvironment.Instance;
82+
environment = GetEnvironment(request, environment);
8283
if (!environment.IsAppService())
8384
{
8485
return false;
@@ -90,6 +91,24 @@ public static bool IsAppServiceInternalRequest(this HttpRequest request, IEnviro
9091
return !request.Headers.Keys.Contains(ScriptConstants.AntaresLogIdHeaderName);
9192
}
9293

94+
public static bool IsPlatformInternalRequest(this HttpRequest request, IEnvironment environment = null)
95+
{
96+
environment = GetEnvironment(request, environment);
97+
if (!environment.IsAppService())
98+
{
99+
return false;
100+
}
101+
102+
var header = request.Headers[ScriptConstants.AntaresPlatformInternal];
103+
string value = header.FirstOrDefault();
104+
return string.Compare(value, "true", StringComparison.OrdinalIgnoreCase) == 0;
105+
}
106+
107+
private static IEnvironment GetEnvironment(HttpRequest request, IEnvironment environment = null)
108+
{
109+
return environment ?? request.HttpContext.RequestServices?.GetService<IEnvironment>() ?? SystemEnvironment.Instance;
110+
}
111+
93112
public static bool IsColdStart(this HttpRequest request)
94113
{
95114
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
@@ -97,6 +97,7 @@ public static class ScriptConstants
9797
public const string AntaresColdStartHeaderName = "X-MS-COLDSTART";
9898
public const string SiteTokenHeaderName = "x-ms-site-restricted-token";
9999
public const string EasyAuthIdentityHeader = "x-ms-client-principal";
100+
public const string AntaresPlatformInternal = "x-ms-platform-internal";
100101
public const string AzureVersionHeader = "x-ms-version";
101102
public const string XIdentityHeader = "X-IDENTITY-HEADER";
102103
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
@@ -168,6 +168,14 @@ public TestFunctionHost(string scriptPath, string logPath,
168168
HttpClient = _testServer.CreateClient();
169169
HttpClient.Timeout = TimeSpan.FromMinutes(5);
170170

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

@@ -205,6 +213,17 @@ public TestFunctionHost(string scriptPath, string logPath,
205213

206214
public HttpClient HttpClient { get; private set; }
207215

216+
/// <summary>
217+
/// Create a new HttpClient without default test configuration.
218+
/// </summary>
219+
public HttpClient CreateHttpClient()
220+
{
221+
var httpClient = _testServer.CreateClient();
222+
httpClient.Timeout = TimeSpan.FromMinutes(5);
223+
224+
return httpClient;
225+
}
226+
208227
public async Task<string> GetMasterKeyAsync()
209228
{
210229
if (!SecretManagerProvider.SecretsEnabled)

0 commit comments

Comments
 (0)