diff --git a/src/WebJobs.Script.WebHost/ContainerManagement/LinuxContainerInitializationHostedService.cs b/src/WebJobs.Script.WebHost/ContainerManagement/LinuxContainerInitializationHostedService.cs index b64563741d..ca91bf2075 100644 --- a/src/WebJobs.Script.WebHost/ContainerManagement/LinuxContainerInitializationHostedService.cs +++ b/src/WebJobs.Script.WebHost/ContainerManagement/LinuxContainerInitializationHostedService.cs @@ -48,8 +48,8 @@ private async Task ApplyStartContextIfPresent(CancellationToken cancellationToke { _logger.LogDebug("Applying host context"); - var encryptedAssignmentContext = JsonConvert.DeserializeObject(startContext); - var assignmentContext = _startupContextProvider.SetContext(encryptedAssignmentContext); + var hostAssignmentRequest = JsonConvert.DeserializeObject(startContext); + var assignmentContext = _startupContextProvider.SetContext(hostAssignmentRequest); await SpecializeMSISideCar(assignmentContext); try diff --git a/src/WebJobs.Script.WebHost/Controllers/InstanceController.cs b/src/WebJobs.Script.WebHost/Controllers/InstanceController.cs index 7b235e1c10..e90b24bcce 100644 --- a/src/WebJobs.Script.WebHost/Controllers/InstanceController.cs +++ b/src/WebJobs.Script.WebHost/Controllers/InstanceController.cs @@ -1,12 +1,14 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for license information. using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Azure.WebJobs.Script.Diagnostics; using Microsoft.Azure.WebJobs.Script.WebHost.Management; using Microsoft.Azure.WebJobs.Script.WebHost.Models; +using Microsoft.Azure.WebJobs.Script.WebHost.Security.Authentication; using Microsoft.Azure.WebJobs.Script.WebHost.Security.Authorization.Policies; using Microsoft.Extensions.Logging; @@ -21,47 +23,81 @@ public class InstanceController : Controller { private readonly IEnvironment _environment; private readonly IInstanceManager _instanceManager; + private readonly IMetricsLogger _metricsLogger; private readonly ILogger _logger; private readonly StartupContextProvider _startupContextProvider; - public InstanceController(IEnvironment environment, IInstanceManager instanceManager, ILoggerFactory loggerFactory, StartupContextProvider startupContextProvider) + public InstanceController(IEnvironment environment, IInstanceManager instanceManager, ILoggerFactory loggerFactory, StartupContextProvider startupContextProvider, IMetricsLogger metricsLogger) { _environment = environment; _instanceManager = instanceManager; _logger = loggerFactory.CreateLogger(); _startupContextProvider = startupContextProvider; + _metricsLogger = metricsLogger; } [HttpPost] [Route("admin/instance/assign")] [Authorize(Policy = PolicyNames.AdminAuthLevel)] - public async Task Assign([FromBody] EncryptedHostAssignmentContext encryptedAssignmentContext) + public async Task Assign([FromBody] HostAssignmentRequest hostAssignmentRequest) { - _logger.LogDebug($"Starting container assignment for host : {Request?.Host}. ContextLength is: {encryptedAssignmentContext.EncryptedContext?.Length}"); + using (_metricsLogger.LatencyEvent(MetricEventNames.LinuxContainerSpecializationMSIInit)) + { + if (hostAssignmentRequest == null) + { + return BadRequest($"{nameof(hostAssignmentRequest)} cannot be null."); + } - var assignmentContext = _startupContextProvider.SetContext(encryptedAssignmentContext); + if (string.IsNullOrEmpty(hostAssignmentRequest.EncryptedContext) && + hostAssignmentRequest.AssignmentContext is null) + { + return BadRequest($"At least one of {nameof(HostAssignmentRequest.AssignmentContext)} or {nameof(HostAssignmentRequest.EncryptedContext)} must be provided."); + } - // before starting the assignment we want to perform as much - // up front validation on the context as possible - string error = await _instanceManager.ValidateContext(assignmentContext); - if (error != null) - { - return StatusCode(StatusCodes.Status400BadRequest, error); - } + if (!string.IsNullOrEmpty(hostAssignmentRequest.EncryptedContext) && + hostAssignmentRequest.AssignmentContext is not null) + { + return BadRequest($"Only one of {nameof(HostAssignmentRequest.AssignmentContext)} or {nameof(HostAssignmentRequest.EncryptedContext)} may be set."); + } - // Wait for Sidecar specialization to complete before returning ok. - // This shouldn't take too long so ok to do this sequentially. - error = await _instanceManager.SpecializeMSISidecar(assignmentContext); - if (error != null) - { - return StatusCode(StatusCodes.Status500InternalServerError, error); - } + if (!string.IsNullOrEmpty(hostAssignmentRequest.EncryptedContext)) + { + _logger.LogDebug("Starting container assignment. ContextLength is {ContextLength}", hostAssignmentRequest.EncryptedContext.Length); + } + else + { + if (!User.HasClaim(SecurityConstants.AssignUnencryptedClaimType, "true")) + { + _logger.LogWarning("Required claims missing for invoking unencrypted assignment"); + return Forbid(); + } + _logger.LogDebug("Starting container assignment."); + } - var succeeded = _instanceManager.StartAssignment(assignmentContext); + var assignmentContext = _startupContextProvider.SetContext(hostAssignmentRequest); - return succeeded - ? Accepted() - : StatusCode(StatusCodes.Status409Conflict, "Instance already assigned"); + // before starting the assignment we want to perform as much + // up front validation on the context as possible + string error = await _instanceManager.ValidateContext(assignmentContext); + if (error != null) + { + return StatusCode(StatusCodes.Status400BadRequest, error); + } + + // Wait for Sidecar specialization to complete before returning ok. + // This shouldn't take too long so ok to do this sequentially. + error = await _instanceManager.SpecializeMSISidecar(assignmentContext); + if (error != null) + { + return StatusCode(StatusCodes.Status500InternalServerError, error); + } + + var succeeded = _instanceManager.StartAssignment(assignmentContext); + + return succeeded + ? Accepted() + : StatusCode(StatusCodes.Status409Conflict, "Instance already assigned"); + } } [HttpGet] diff --git a/src/WebJobs.Script.WebHost/Models/EncryptedHostAssignmentContext.cs b/src/WebJobs.Script.WebHost/Models/EncryptedHostAssignmentContext.cs deleted file mode 100644 index 95d1806713..0000000000 --- a/src/WebJobs.Script.WebHost/Models/EncryptedHostAssignmentContext.cs +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT License. See License.txt in the project root for license information. - -using Newtonsoft.Json; - -namespace Microsoft.Azure.WebJobs.Script.WebHost.Models -{ - public class EncryptedHostAssignmentContext - { - [JsonProperty("encryptedContext")] - public string EncryptedContext { get; set; } - } -} diff --git a/src/WebJobs.Script.WebHost/Models/HostAssignmentRequest.cs b/src/WebJobs.Script.WebHost/Models/HostAssignmentRequest.cs new file mode 100644 index 0000000000..535b617bef --- /dev/null +++ b/src/WebJobs.Script.WebHost/Models/HostAssignmentRequest.cs @@ -0,0 +1,16 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using Newtonsoft.Json; + +namespace Microsoft.Azure.WebJobs.Script.WebHost.Models +{ + public sealed class HostAssignmentRequest + { + [JsonProperty("encryptedContext", DefaultValueHandling = DefaultValueHandling.Ignore)] + public string EncryptedContext { get; set; } + + [JsonProperty("assignmentContext", DefaultValueHandling = DefaultValueHandling.Ignore)] + public HostAssignmentContext AssignmentContext { get; set; } + } +} diff --git a/src/WebJobs.Script.WebHost/Security/Authentication/Jwt/ScriptJwtBearerExtensions.cs b/src/WebJobs.Script.WebHost/Security/Authentication/Jwt/ScriptJwtBearerExtensions.cs index cfda6d80d9..40c5a2f86d 100644 --- a/src/WebJobs.Script.WebHost/Security/Authentication/Jwt/ScriptJwtBearerExtensions.cs +++ b/src/WebJobs.Script.WebHost/Security/Authentication/Jwt/ScriptJwtBearerExtensions.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for license information. using System; @@ -72,6 +72,11 @@ private static void ConfigureOptions(JwtBearerOptions options) claims.Add(new Claim(SecurityConstants.InvokeClaimType, "true")); } + if (string.Equals(c.SecurityToken.Issuer, ScriptConstants.LegionCoreUri, StringComparison.OrdinalIgnoreCase)) + { + claims.Add(new Claim(SecurityConstants.AssignUnencryptedClaimType, "true")); + } + c.Principal.AddIdentity(new ClaimsIdentity(claims)); c.Success(); @@ -149,6 +154,7 @@ public static TokenValidationParameters CreateTokenValidationParameters() result.ValidIssuers = [ AppServiceCoreUri, + LegionCoreUri, string.Format(ScmSiteUriFormat, ScriptSettingsManager.Instance.GetSetting(AzureWebsiteName)), string.Format(SiteUriFormat, ScriptSettingsManager.Instance.GetSetting(AzureWebsiteName)) ]; diff --git a/src/WebJobs.Script.WebHost/Security/Authentication/SecurityConstants.cs b/src/WebJobs.Script.WebHost/Security/Authentication/SecurityConstants.cs index 152151cb0b..7d7009709d 100644 --- a/src/WebJobs.Script.WebHost/Security/Authentication/SecurityConstants.cs +++ b/src/WebJobs.Script.WebHost/Security/Authentication/SecurityConstants.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for license information. using System; @@ -20,5 +20,10 @@ public class SecurityConstants /// /admin/functions/{function} API. /// public const string InvokeClaimType = "http://schemas.microsoft.com/2017/07/functions/claims/invoke"; + + /// + /// Claim indicating whether a principal is authorized to invoke assign with an unencrypted payload. + /// + public const string AssignUnencryptedClaimType = "http://schemas.microsoft.com/2017/07/functions/claims/assign-unencrypted"; } } diff --git a/src/WebJobs.Script.WebHost/StartupContextProvider.cs b/src/WebJobs.Script.WebHost/StartupContextProvider.cs index 25c1030bb9..ff0e78cfe7 100644 --- a/src/WebJobs.Script.WebHost/StartupContextProvider.cs +++ b/src/WebJobs.Script.WebHost/StartupContextProvider.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for license information. using System; @@ -132,15 +132,20 @@ private StartupContext GetStartupContextOrNull() } /// - /// Decrypt and deserialize the specified context, and apply values from it to the - /// startup cache context. + /// Applies the values from the specified assignment request to the startup cache context, + /// performing any required decryption. /// - /// The encrypted assignment context. - /// The decrypted assignment context - public virtual HostAssignmentContext SetContext(EncryptedHostAssignmentContext encryptedContext) + /// The Host assignment request. + /// The assignment context applied. + public virtual HostAssignmentContext SetContext(HostAssignmentRequest hostAssignmentRequest) { - string decryptedContext = EncryptionHelper.Decrypt(encryptedContext.EncryptedContext, environment: _environment); - var hostAssignmentContext = JsonConvert.DeserializeObject(decryptedContext); + var hostAssignmentContext = hostAssignmentRequest.AssignmentContext; + + if (!string.IsNullOrEmpty(hostAssignmentRequest.EncryptedContext)) + { + string decryptedContext = EncryptionHelper.Decrypt(hostAssignmentRequest.EncryptedContext, environment: _environment); + hostAssignmentContext = JsonConvert.DeserializeObject(decryptedContext); + } // Don't update StartupContext for warmup requests if (!hostAssignmentContext.IsWarmupRequest) diff --git a/src/WebJobs.Script/Diagnostics/MetricEventNames.cs b/src/WebJobs.Script/Diagnostics/MetricEventNames.cs index a0aa9748e5..be0c56a2f1 100644 --- a/src/WebJobs.Script/Diagnostics/MetricEventNames.cs +++ b/src/WebJobs.Script/Diagnostics/MetricEventNames.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for license information. namespace Microsoft.Azure.WebJobs.Script.Diagnostics @@ -83,6 +83,7 @@ public static class MetricEventNames public const string SecretManagerPurgeOldSecrets = "secretmanager.purgeoldsecrets.{0}"; // Linux container specialization events + public const string LinuxContainerSpecializationAssign = "linux.container.specialization.assign"; public const string LinuxContainerSpecializationBindMount = "linux.container.specialization.bind.mount"; public const string LinuxContainerSpecializationMountCifs = "linux.container.specialization.mount.cifs"; public const string LinuxContainerSpecializationZipExtract = "linux.container.specialization.zip.extract"; diff --git a/src/WebJobs.Script/ScriptConstants.cs b/src/WebJobs.Script/ScriptConstants.cs index c050973d30..46d6441c39 100644 --- a/src/WebJobs.Script/ScriptConstants.cs +++ b/src/WebJobs.Script/ScriptConstants.cs @@ -152,6 +152,7 @@ public static class ScriptConstants public const string ScmSiteUriFormat = "https://{0}.scm.azurewebsites.net"; public const string SiteUriFormat = "https://{0}.azurewebsites.net"; public const string AppServiceCoreUri = "https://appservice.core.azurewebsites.net"; + public const string LegionCoreUri = "https://legion.core.azurewebsites.net"; public const string AzureFunctionsSystemDirectoryName = ".azurefunctions"; public const string HttpMethodConstraintName = "httpMethod"; diff --git a/test/WebJobs.Script.Tests.Integration/Host/StandbyManager/StandbyManagerE2ETests_Linux.cs b/test/WebJobs.Script.Tests.Integration/Host/StandbyManager/StandbyManagerE2ETests_Linux.cs index cd76ec5bb2..4a9503ff76 100644 --- a/test/WebJobs.Script.Tests.Integration/Host/StandbyManager/StandbyManagerE2ETests_Linux.cs +++ b/test/WebJobs.Script.Tests.Integration/Host/StandbyManager/StandbyManagerE2ETests_Linux.cs @@ -70,8 +70,10 @@ public async Task StandbyModeE2E_LinuxConsumptionOnLegion() Assert.Equal(typeof(LinuxContainerLegionMetricsPublisher), webHost.Services.GetRequiredService().GetType()); } - [Fact] - public async Task StandbyModeE2E_LinuxContainer() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task StandbyModeE2E_LinuxContainer(bool useEncryptedPayload) { byte[] bytes = TestHelpers.GenerateKeyBytes(); var encryptionKey = Convert.ToBase64String(bytes); @@ -114,7 +116,7 @@ public async Task StandbyModeE2E_LinuxContainer() await VerifyWarmupSucceeds(restart: true); // now specialize the site - await Assign(encryptionKey); + await Assign(encryptionKey, useEncryptedPayload); // immediately call a function - expect the call to block until // the host is fully specialized @@ -240,7 +242,7 @@ public async Task LinuxContainer_TimeZoneEnvVariableE2E() } - private async Task Assign(string encryptionKey) + private async Task Assign(string encryptionKey, bool useEncryptedPayload) { // create a zip package var testFunctionPath = Path.Combine("TestScripts", "Node", "HttpTrigger"); @@ -260,18 +262,18 @@ private async Task Assign(string encryptionKey) string uri = "admin/instance/assign"; var request = new HttpRequestMessage(HttpMethod.Post, uri); var environment = new Dictionary() - { - { EnvironmentSettingNames.AzureWebsiteZipDeployment, sasUri.ToString() }, - { RpcWorkerConstants.FunctionWorkerRuntimeVersionSettingName, "~2" }, - { EnvironmentSettingNames.FunctionWorkerRuntime, "node" } - }; + { + { EnvironmentSettingNames.AzureWebsiteZipDeployment, sasUri.ToString() }, + { RpcWorkerConstants.FunctionWorkerRuntimeVersionSettingName, "~2" }, + { EnvironmentSettingNames.FunctionWorkerRuntime, "node" } + }; var assignmentContext = new HostAssignmentContext { SiteId = 1234, SiteName = "TestApp", Environment = environment }; - var encryptedAssignmentContext = CreateEncryptedContext(assignmentContext, encryptionKey); + var encryptedAssignmentContext = CreateHostAssignmentRequest(assignmentContext, encryptionKey, useEncryptedPayload); string json = JsonConvert.SerializeObject(encryptedAssignmentContext); request.Content = new StringContent(json, Encoding.UTF8, "application/json"); request.Headers.Add(AuthenticationLevelHandler.FunctionsKeyHeaderName, masterKey); @@ -279,13 +281,17 @@ private async Task Assign(string encryptionKey) Assert.Equal(HttpStatusCode.Accepted, response.StatusCode); } - private static EncryptedHostAssignmentContext CreateEncryptedContext(HostAssignmentContext context, string key) + private static HostAssignmentRequest CreateHostAssignmentRequest(HostAssignmentContext context, string key, bool useEncryptedPayload) { + if (!useEncryptedPayload) + { + return new HostAssignmentRequest { AssignmentContext = context }; + } + string json = JsonConvert.SerializeObject(context); var encryptionKey = Convert.FromBase64String(key); string encrypted = EncryptionHelper.Encrypt(json, encryptionKey); - - return new EncryptedHostAssignmentContext { EncryptedContext = encrypted }; + return new HostAssignmentRequest { EncryptedContext = encrypted }; } } } diff --git a/test/WebJobs.Script.Tests.Integration/Management/InstanceControllerTests.cs b/test/WebJobs.Script.Tests.Integration/Management/InstanceControllerTests.cs index 4895727dbe..bbdec850c3 100644 --- a/test/WebJobs.Script.Tests.Integration/Management/InstanceControllerTests.cs +++ b/test/WebJobs.Script.Tests.Integration/Management/InstanceControllerTests.cs @@ -1,12 +1,7 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for license information. -using System; -using System.Collections.Generic; -using System.Net; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Azure.WebJobs.Script.WebHost; using Microsoft.Azure.WebJobs.Script.WebHost.Controllers; @@ -14,11 +9,19 @@ using Microsoft.Azure.WebJobs.Script.WebHost.Management.LinuxSpecialization; using Microsoft.Azure.WebJobs.Script.WebHost.Models; using Microsoft.Azure.WebJobs.Script.WebHost.Security; +using Microsoft.Azure.WebJobs.Script.WebHost.Security.Authentication; using Microsoft.Extensions.Logging; using Microsoft.WebJobs.Script.Tests; using Moq; using Moq.Protected; using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Security.Claims; +using System.Threading; +using System.Threading.Tasks; using Xunit; namespace Microsoft.Azure.WebJobs.Script.Tests.Managment @@ -67,7 +70,7 @@ public async Task Assign_MSISpecializationFailure_ReturnsError() instanceManager.Reset(); - var instanceController = new InstanceController(environment, instanceManager, loggerFactory, startupContextProvider); + var instanceController = new InstanceController(environment, instanceManager, loggerFactory, startupContextProvider, new TestMetricsLogger()); var hostAssignmentContext = new HostAssignmentContext { @@ -80,14 +83,14 @@ public async Task Assign_MSISpecializationFailure_ReturnsError() var encryptedHostAssignmentValue = EncryptionHelper.Encrypt(JsonConvert.SerializeObject(hostAssignmentContext), TestHelpers.EncryptionKey.ToKeyBytes()); - var encryptedHostAssignmentContext = new EncryptedHostAssignmentContext() + var hostAssignmentRequest = new HostAssignmentRequest() { EncryptedContext = encryptedHostAssignmentValue }; environment.SetEnvironmentVariable(EnvironmentSettingNames.ContainerEncryptionKey, TestHelpers.EncryptionKey); - IActionResult result = await instanceController.Assign(encryptedHostAssignmentContext); + IActionResult result = await instanceController.Assign(hostAssignmentRequest); var objectResult = result as ObjectResult; @@ -105,7 +108,7 @@ public void Http_Health_Status_Returns_Ok() var loggerProvider = new TestLoggerProvider(); loggerFactory.AddProvider(loggerProvider); - var instanceController = new InstanceController(null, null, loggerFactory, null); + var instanceController = new InstanceController(null, null, loggerFactory, null, new TestMetricsLogger()); var actionResult = instanceController.GetHttpHealthStatus(); var okResult = actionResult as OkResult; @@ -142,7 +145,7 @@ public async Task Assignment_Sets_Secrets_Context() instanceManager.Reset(); - var instanceController = new InstanceController(environment, instanceManager, loggerFactory, startupContextProvider); + var instanceController = new InstanceController(environment, instanceManager, loggerFactory, startupContextProvider, new TestMetricsLogger()); var hostAssignmentContext = new HostAssignmentContext { @@ -156,14 +159,14 @@ public async Task Assignment_Sets_Secrets_Context() var encryptedHostAssignmentValue = EncryptionHelper.Encrypt(JsonConvert.SerializeObject(hostAssignmentContext), TestHelpers.EncryptionKey.ToKeyBytes()); - var encryptedHostAssignmentContext = new EncryptedHostAssignmentContext() + var hostAssignmentRequest = new HostAssignmentRequest() { EncryptedContext = encryptedHostAssignmentValue }; environment.SetEnvironmentVariable(EnvironmentSettingNames.ContainerEncryptionKey, TestHelpers.EncryptionKey); - await instanceController.Assign(encryptedHostAssignmentContext); + await instanceController.Assign(hostAssignmentRequest); Assert.NotNull(startupContextProvider.Context); } @@ -195,7 +198,7 @@ public async Task Assignment_Does_Not_Set_Secrets_Context_For_Warmup_Request() instanceManager.Reset(); - var instanceController = new InstanceController(environment, instanceManager, loggerFactory, startupContextProvider); + var instanceController = new InstanceController(environment, instanceManager, loggerFactory, startupContextProvider, new TestMetricsLogger()); var hostAssignmentContext = new HostAssignmentContext { @@ -209,21 +212,23 @@ public async Task Assignment_Does_Not_Set_Secrets_Context_For_Warmup_Request() var encryptedHostAssignmentValue = EncryptionHelper.Encrypt(JsonConvert.SerializeObject(hostAssignmentContext), TestHelpers.EncryptionKey.ToKeyBytes()); - var encryptedHostAssignmentContext = new EncryptedHostAssignmentContext() + var hostAssignmentRequest = new HostAssignmentRequest() { EncryptedContext = encryptedHostAssignmentValue }; environment.SetEnvironmentVariable(EnvironmentSettingNames.ContainerEncryptionKey, TestHelpers.EncryptionKey); - await instanceController.Assign(encryptedHostAssignmentContext); + await instanceController.Assign(hostAssignmentRequest); Assert.Null(startupContextProvider.Context); } [Theory] - [InlineData(true, true)] - [InlineData(false, true)] - public async Task Assignment_Invokes_InstanceManager_Methods_For_Warmup_Requests_Also(bool isWarmupRequest, bool shouldInvokeMethod) + [InlineData(true, true, true)] + [InlineData(false, true, true)] + [InlineData(true, true, false)] + [InlineData(false, true, false)] + public async Task Assignment_Invokes_InstanceManager_Methods_For_Warmup_Requests_Also(bool isWarmupRequest, bool shouldInvokeMethod, bool useEncryptedPayload) { var environment = new TestEnvironment(); environment.SetEnvironmentVariable(EnvironmentSettingNames.AzureWebsitePlaceholderMode, "1"); @@ -238,7 +243,12 @@ public async Task Assignment_Invokes_InstanceManager_Methods_For_Warmup_Requests instanceManager.Reset(); var instanceController = new InstanceController(environment, instanceManager.Object, loggerFactory, - startupContextProvider); + startupContextProvider, new TestMetricsLogger()); + DefaultHttpContext context = new() { User = new ClaimsPrincipal(new ClaimsIdentity(new Claim[] + { + new Claim(SecurityConstants.AssignUnencryptedClaimType, "true") + })) }; + instanceController.ControllerContext = new() { HttpContext = context }; var hostAssignmentContext = new HostAssignmentContext { @@ -250,14 +260,18 @@ public async Task Assignment_Invokes_InstanceManager_Methods_For_Warmup_Requests EncryptionHelper.Encrypt(JsonConvert.SerializeObject(hostAssignmentContext), TestHelpers.EncryptionKey.ToKeyBytes()); - var encryptedHostAssignmentContext = new EncryptedHostAssignmentContext() + var hostAssignmentRequest = new HostAssignmentRequest() { }; + if (useEncryptedPayload) { - EncryptedContext = encryptedHostAssignmentValue - }; - + hostAssignmentRequest.EncryptedContext = encryptedHostAssignmentValue; + } + else + { + hostAssignmentRequest.AssignmentContext = hostAssignmentContext; + } environment.SetEnvironmentVariable(EnvironmentSettingNames.ContainerEncryptionKey, TestHelpers.EncryptionKey); - await instanceController.Assign(encryptedHostAssignmentContext); + await instanceController.Assign(hostAssignmentRequest); instanceManager.Verify(i => i.ValidateContext(It.IsAny()), shouldInvokeMethod ? Times.Once() : Times.Never()); @@ -266,5 +280,43 @@ public async Task Assignment_Invokes_InstanceManager_Methods_For_Warmup_Requests instanceManager.Verify(i => i.StartAssignment(It.IsAny()), shouldInvokeMethod ? Times.Once() : Times.Never()); } + + [Fact] + public async Task Assignment_ErrorScenarios() + { + var environment = new TestEnvironment(); + environment.SetEnvironmentVariable(EnvironmentSettingNames.AzureWebsitePlaceholderMode, "1"); + var loggerFactory = new LoggerFactory(); + var loggerProvider = new TestLoggerProvider(); + loggerFactory.AddProvider(loggerProvider); + var instanceController = new InstanceController(environment, null, loggerFactory, null, new TestMetricsLogger()); + + // HostAssignmentRequest is null + var result = await instanceController.Assign(null); + var badRequestResult = result as BadRequestObjectResult; + Assert.NotNull(badRequestResult); + Assert.Equal(400, badRequestResult.StatusCode); + Assert.Equal("hostAssignmentRequest cannot be null.", badRequestResult.Value); + + // Both encrypted and unencrypted context are null + var hostAssignmentRequest = new HostAssignmentRequest() { }; + result = await instanceController.Assign(hostAssignmentRequest); + badRequestResult = result as BadRequestObjectResult; + Assert.NotNull(badRequestResult); + Assert.Equal(400, badRequestResult.StatusCode); + Assert.Equal("At least one of AssignmentContext or EncryptedContext must be provided.", badRequestResult.Value); + + // Both encrypted and unencrypted context are set + hostAssignmentRequest = new HostAssignmentRequest() + { + EncryptedContext = "EncryptedContext", + AssignmentContext = new HostAssignmentContext() + }; + result = await instanceController.Assign(hostAssignmentRequest); + badRequestResult = result as BadRequestObjectResult; + Assert.NotNull(badRequestResult); + Assert.Equal(400, badRequestResult.StatusCode); + Assert.Equal("Only one of AssignmentContext or EncryptedContext may be set.", badRequestResult.Value); + } } } diff --git a/test/WebJobs.Script.Tests/ContainerManagment/LinuxContainerInitializationHostedServiceTests.cs b/test/WebJobs.Script.Tests/ContainerManagment/LinuxContainerInitializationHostedServiceTests.cs index 475fdb10a8..7f73d46790 100644 --- a/test/WebJobs.Script.Tests/ContainerManagment/LinuxContainerInitializationHostedServiceTests.cs +++ b/test/WebJobs.Script.Tests/ContainerManagment/LinuxContainerInitializationHostedServiceTests.cs @@ -37,7 +37,7 @@ public async Task AssignInstanceAsyncIsAwaitedTest() _mockInstanceManager.Setup(m => m.AssignInstanceAsync(It.IsAny())).Returns(tcs.Task); var assignmentContext = new HostAssignmentContext(); - _mockStartupContextProvider.Setup(p => p.SetContext(It.IsAny())).Returns(assignmentContext); + _mockStartupContextProvider.Setup(p => p.SetContext(It.IsAny())).Returns(assignmentContext); var service = new TestLinuxContainerInitializationHostedService(GetTestEnvironment(), _mockInstanceManager.Object, _mockLogger.Object, _mockStartupContextProvider.Object); @@ -65,9 +65,9 @@ public TestLinuxContainerInitializationHostedService(IEnvironment environment, I protected override Task<(bool HasStartContext, string StartContext)> TryGetStartContextOrNullAsync(CancellationToken cancellationToken) { - var encryptedAssignmentContext = new EncryptedHostAssignmentContext { EncryptedContext = "test" }; + var hostAssignmentRequest = new HostAssignmentRequest { EncryptedContext = "test" }; - return Task.FromResult((true, JsonConvert.SerializeObject(encryptedAssignmentContext))); + return Task.FromResult((true, JsonConvert.SerializeObject(hostAssignmentRequest))); } protected override Task SpecializeMSISideCar(HostAssignmentContext assignmentContext) diff --git a/test/WebJobs.Script.Tests/StartupContextProviderTests.cs b/test/WebJobs.Script.Tests/StartupContextProviderTests.cs index 70c93ed805..f6742c6074 100644 --- a/test/WebJobs.Script.Tests/StartupContextProviderTests.cs +++ b/test/WebJobs.Script.Tests/StartupContextProviderTests.cs @@ -188,7 +188,7 @@ public void SetContext_AppliesHostAssignmentContext() }; string json = JsonConvert.SerializeObject(context); string encrypted = EncryptionHelper.Encrypt(json, environment: _environment); - var encryptedContext = new EncryptedHostAssignmentContext { EncryptedContext = encrypted }; + var encryptedContext = new HostAssignmentRequest { EncryptedContext = encrypted }; var result = _startupContextProvider.SetContext(encryptedContext); Assert.Equal(context.SiteName, result.SiteName); @@ -212,9 +212,9 @@ public void Does_Not_SetContext_AppliesHostAssignmentContext_For_Warmup_Request( }; string json = JsonConvert.SerializeObject(context); string encrypted = EncryptionHelper.Encrypt(json, environment: _environment); - var encryptedContext = new EncryptedHostAssignmentContext { EncryptedContext = encrypted }; + var hostAssignmentRequest = new HostAssignmentRequest { EncryptedContext = encrypted }; - var result = _startupContextProvider.SetContext(encryptedContext); + var result = _startupContextProvider.SetContext(hostAssignmentRequest); Assert.Equal(context.SiteName, result.SiteName); Assert.Equal(_secrets.Host.Master, result.Secrets.Host.Master);