Skip to content

Commit 9bda97e

Browse files
committed
Add resource filter for ARM Extension API requests
1 parent a8d43ce commit 9bda97e

File tree

11 files changed

+243
-3
lines changed

11 files changed

+243
-3
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
namespace Microsoft.Azure.WebJobs.Script.WebHost.Controllers
2121
{
2222
[Authorize(Policy = PolicyNames.AdminAuthLevel)]
23+
[ResourceContainsSecrets]
2324
public class KeysController : Controller
2425
{
2526
private const string MasterKeyName = "_master";
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
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.Threading.Tasks;
6+
using Microsoft.AspNetCore.Http;
7+
using Microsoft.AspNetCore.Mvc.Controllers;
8+
using Microsoft.AspNetCore.Mvc.Filters;
9+
using Microsoft.Azure.WebJobs.Script.Extensions;
10+
using Microsoft.Azure.WebJobs.Script.WebHost.Properties;
11+
12+
namespace Microsoft.Azure.WebJobs.Script.WebHost.Filters
13+
{
14+
/// <summary>
15+
/// Resource filter used to ensure secrets aren't returned for GET requests made via the Functions ARM extension
16+
/// API (hostruntime), unless properly authorized.
17+
/// </summary>
18+
/// <remarks>
19+
/// All our first class ARM APIs handle RBAC naturally. For the hostruntime bridge, the runtime collaborates
20+
/// based on request details coming from ARM/Geo.
21+
/// </remarks>
22+
public sealed class ArmExtensionResourceFilter : IAsyncResourceFilter
23+
{
24+
public async Task OnResourceExecutionAsync(ResourceExecutingContext context, ResourceExecutionDelegate next)
25+
{
26+
// We only want to apply this filter for GET extension ARM requests that were forwarded directly to us via
27+
// hostruntime bridge, not hostruntime requests initiated internally by the geomaster. The latter requests
28+
// won't have the x-ms-arm-request-tracking-id header.
29+
var request = context.HttpContext.Request;
30+
bool isArmExtensionRequest = request.HasHeader(ScriptConstants.AntaresARMRequestTrackingIdHeader) &&
31+
request.HasHeader(ScriptConstants.AntaresARMExtensionsRouteHeader);
32+
33+
if (isArmExtensionRequest && string.Equals(request.Method, "GET", StringComparison.OrdinalIgnoreCase))
34+
{
35+
// requests made by owner/co-admin are not filtered
36+
if (!request.HasHeaderValue(ScriptConstants.AntaresClientAuthorizationSourceHeader, "legacy"))
37+
{
38+
var controllerActionDescriptor = context.ActionDescriptor as ControllerActionDescriptor;
39+
if (controllerActionDescriptor != null && controllerActionDescriptor.MethodInfo != null &&
40+
Utility.GetHierarchicalAttributeOrNull<ResourceContainsSecretsAttribute>(controllerActionDescriptor.MethodInfo) != null)
41+
{
42+
// if the resource returned by the action contains secrets, fail the request
43+
context.HttpContext.Response.StatusCode = StatusCodes.Status401Unauthorized;
44+
await context.HttpContext.Response.WriteAsync(Resources.UnauthorizedArmExtensionResourceRequest);
45+
return;
46+
}
47+
}
48+
}
49+
50+
await next();
51+
}
52+
}
53+
}

src/WebJobs.Script.WebHost/Properties/Resources.Designer.cs

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/WebJobs.Script.WebHost/Properties/Resources.resx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,4 +294,7 @@
294294
<data name="TraceStaleHostSecretRefresh" xml:space="preserve">
295295
<value>Stale host secrets detected. Refreshing secrets.</value>
296296
</data>
297+
<data name="UnauthorizedArmExtensionResourceRequest" xml:space="preserve">
298+
<value>GET requests for this resource via the hostruntime extensions API are not authorized. Please use an alternate first class ARM API.</value>
299+
</data>
297300
</root>
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
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+
6+
namespace Microsoft.Azure.WebJobs.Script.WebHost
7+
{
8+
/// <summary>
9+
/// Attribute applied to actions to indicate whether the resource returned by an action
10+
/// contains secrets.
11+
/// </summary>
12+
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
13+
public class ResourceContainsSecretsAttribute : Attribute
14+
{
15+
}
16+
}

src/WebJobs.Script.WebHost/WebHostServiceCollectionExtensions.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
using Microsoft.Azure.WebJobs.Script.WebHost.ContainerManagement;
1414
using Microsoft.Azure.WebJobs.Script.WebHost.DependencyInjection;
1515
using Microsoft.Azure.WebJobs.Script.WebHost.Diagnostics;
16+
using Microsoft.Azure.WebJobs.Script.WebHost.Filters;
1617
using Microsoft.Azure.WebJobs.Script.WebHost.Management;
1718
using Microsoft.Azure.WebJobs.Script.WebHost.Metrics;
1819
using Microsoft.Azure.WebJobs.Script.WebHost.Middleware;
@@ -65,8 +66,11 @@ public static void AddWebJobsScriptHost(this IServiceCollection services, IConfi
6566
{
6667
services.AddHttpContextAccessor();
6768
services.AddWebJobsScriptHostRouting();
68-
services.AddMvc()
69-
.AddXmlDataContractSerializerFormatters();
69+
services.AddMvc(options =>
70+
{
71+
options.Filters.Add(new ArmExtensionResourceFilter());
72+
})
73+
.AddXmlDataContractSerializerFormatters();
7074

7175
// Standby services
7276
services.AddStandbyServices();

src/WebJobs.Script/Extensions/HttpRequestExtensions.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,16 @@ public static string GetRequestId(this HttpRequest request)
3737
return request.GetRequestPropertyOrDefault<string>(ScriptConstants.AzureFunctionsRequestIdKey);
3838
}
3939

40+
public static bool HasHeader(this HttpRequest request, string headerName)
41+
{
42+
return !string.IsNullOrEmpty(request.GetHeaderValueOrDefault(headerName));
43+
}
44+
45+
public static bool HasHeaderValue(this HttpRequest request, string headerName, string value)
46+
{
47+
return string.Equals(request.GetHeaderValueOrDefault(headerName), value, StringComparison.OrdinalIgnoreCase);
48+
}
49+
4050
public static string GetHeaderValueOrDefault(this HttpRequest request, string headerName)
4151
{
4252
StringValues values;

src/WebJobs.Script/ScriptConstants.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,9 @@ public static class ScriptConstants
8383

8484
public const string FunctionsUserAgent = "AzureFunctionsRuntime";
8585
public const string AntaresDefaultHostNameHeader = "WAS-DEFAULT-HOSTNAME";
86+
public const string AntaresARMRequestTrackingIdHeader = "x-ms-arm-request-tracking-id";
87+
public const string AntaresARMExtensionsRouteHeader = "X-MS-VIA-EXTENSIONS-ROUTE";
88+
public const string AntaresClientAuthorizationSourceHeader = "X-MS-CLIENT-AUTHORIZATION-SOURCE";
8689
public const string AntaresLogIdHeaderName = "X-ARR-LOG-ID";
8790
public const string AntaresScaleOutHeaderName = "X-FUNCTION-SCALEOUT";
8891
public const string AntaresColdStartHeaderName = "X-MS-COLDSTART";

src/WebJobs.Script/Utility.cs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,39 @@ public static class Utility
4444

4545
private static List<string> dotNetLanguages = new List<string>() { DotNetScriptTypes.CSharp, DotNetScriptTypes.DotNetAssembly };
4646

47+
/// <summary>
48+
/// Walk from the method up to the containing type, looking for an instance
49+
/// of the specified attribute type, returning it if found.
50+
/// </summary>
51+
/// <param name="method">The method to check.</param>
52+
internal static T GetHierarchicalAttributeOrNull<T>(MethodInfo method) where T : Attribute
53+
{
54+
return (T)GetHierarchicalAttributeOrNull(method, typeof(T));
55+
}
56+
57+
/// <summary>
58+
/// Walk from the method up to the containing type, looking for an instance
59+
/// of the specified attribute type, returning it if found.
60+
/// </summary>
61+
/// <param name="method">The method to check.</param>
62+
/// <param name="type">The attribute type to look for.</param>
63+
internal static Attribute GetHierarchicalAttributeOrNull(MethodInfo method, Type type)
64+
{
65+
var attribute = method.GetCustomAttribute(type);
66+
if (attribute != null)
67+
{
68+
return attribute;
69+
}
70+
71+
attribute = method.DeclaringType.GetCustomAttribute(type);
72+
if (attribute != null)
73+
{
74+
return attribute;
75+
}
76+
77+
return null;
78+
}
79+
4780
internal static async Task InvokeWithRetriesAsync(Action action, int maxRetries, TimeSpan retryInterval)
4881
{
4982
await InvokeWithRetriesAsync(() =>

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ protected EndToEndTestFixture(string rootPath, string testId, string functionsWo
7070

7171
public TestEventGenerator EventGenerator { get; private set; } = new TestEventGenerator();
7272

73+
public string MasterKey { get; private set; }
74+
7375
protected virtual ExtensionPackageReference[] GetExtensionsToInstall()
7476
{
7577
return null;
@@ -130,6 +132,8 @@ public async Task InitializeAsync()
130132
TableClient = storageAccount.CreateCloudTableClient();
131133

132134
await CreateTestStorageEntities();
135+
136+
MasterKey = await Host.GetMasterKeyAsync();
133137
}
134138

135139
public virtual void ConfigureScriptHost(IWebJobsBuilder webJobsBuilder)

0 commit comments

Comments
 (0)