Skip to content

Commit eb800ee

Browse files
Session Context Bug fix (#3724)
## Why make this change? Solves bug related to how we handle specific queries with session-context. ## What is this change? - Sets the claims inside the `X-MS-CLIENT-PRINCIPAL` header as parameters before using them inside of a query. ## How was this tested? - [x] Integration Tests - [ ] Unit Tests Added test that ensures that invalid queries are not run through headers with session context. ## Sample Request(s) N/A
1 parent 091a627 commit eb800ee

3 files changed

Lines changed: 116 additions & 2 deletions

File tree

src/Core/Resolvers/MsSqlQueryExecutor.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -516,6 +516,7 @@ public override string GetSessionParamsQuery(HttpContext? httpContext, IDictiona
516516

517517
// Counter to generate different param name for each of the sessionParam.
518518
IncrementingInteger counter = new();
519+
const string SESSION_KEY_NAME = $"{BaseQueryStructure.PARAM_NAME_PREFIX}session_key";
519520
const string SESSION_PARAM_NAME = $"{BaseQueryStructure.PARAM_NAME_PREFIX}session_param";
520521
StringBuilder sessionMapQuery = new();
521522

@@ -528,10 +529,14 @@ public override string GetSessionParamsQuery(HttpContext? httpContext, IDictiona
528529

529530
foreach ((string claimType, string claimValue) in sessionParams)
530531
{
532+
string keyName = $"{SESSION_KEY_NAME}{counter.Current()}";
533+
parameters.Add(keyName, new(claimType));
534+
531535
string paramName = $"{SESSION_PARAM_NAME}{counter.Next()}";
532536
parameters.Add(paramName, new(claimValue));
537+
533538
// Append statement to set read only param value - can be set only once for a connection.
534-
string statementToSetReadOnlyParam = "EXEC sp_set_session_context " + $"'{claimType}', " + paramName + ", @read_only = 0;";
539+
string statementToSetReadOnlyParam = "EXEC sp_set_session_context " + keyName + ", " + paramName + ", @read_only = 0;";
535540
sessionMapQuery = sessionMapQuery.Append(statementToSetReadOnlyParam);
536541
}
537542
}

src/Service.Tests/Authentication/EasyAuthAuthenticationUnitTests.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -325,7 +325,6 @@ public async Task TestValidStaticWebAppsEasyAuthTokenWithAnonymousRoleOnly()
325325
DisplayName = "Anonymous role - X-MS-API-ROLE is not honored")]
326326
[DataRow(true, "author",
327327
DisplayName = "Authenticated role - existing X-MS-API-ROLE is honored")]
328-
[TestMethod]
329328
public async Task TestClientRoleHeaderPresence(bool addAuthenticated, string clientRoleHeader)
330329
{
331330
string generatedToken = AuthTestHelper.CreateStaticWebAppsEasyAuthToken(addAuthenticated);

src/Service.Tests/SqlTests/RestApiTests/Find/MsSqlFindApiTests.cs

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33

4+
using System;
45
using System.Collections.Generic;
6+
using System.IO;
7+
using System.Net;
8+
using System.Net.Http;
59
using System.Threading.Tasks;
10+
using Azure.DataApiBuilder.Config.ObjectModel;
11+
using Azure.DataApiBuilder.Core.Authorization;
12+
using Azure.DataApiBuilder.Core.Configurations;
13+
using Microsoft.AspNetCore.TestHost;
614
using Microsoft.VisualStudio.TestTools.UnitTesting;
715

816
namespace Azure.DataApiBuilder.Service.Tests.SqlTests.RestApiTests.Find
@@ -661,5 +669,107 @@ await SetupAndRunRestApiTest(
661669
);
662670
}
663671
#endregion
672+
673+
#region RestApiTests Outliers
674+
675+
/// <summary>
676+
/// Tests we ensure that an invalid query in the EasyAuth header
677+
/// retruns a successful request without executing the invalid query.
678+
/// </summary>
679+
[TestCategory(TestCategory.MSSQL)]
680+
[TestMethod]
681+
public async Task TestInvalidQueryInHeader()
682+
{
683+
TestHelper.SetupDatabaseEnvironment(TestCategory.MSSQL);
684+
685+
string firstHeader = @"
686+
{
687+
""auth_typ"":""aad"",
688+
""claims"":[
689+
{
690+
""typ"":""x', N'v';SET IDENTITY_INSERT authors ON;--"",
691+
""val"":""x""
692+
}
693+
],
694+
""UserRoles"":[""authenticated""]
695+
}";
696+
697+
string firstGeneratedToken = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(firstHeader));
698+
699+
string secondHeader = @"
700+
{
701+
""auth_typ"":""aad"",
702+
""claims"":[
703+
{
704+
""typ"":""x', N'v';INSERT INTO authors (id, name, birthdate) VALUES (10001, 'Hidden Author', '2001-01-01');--"",
705+
""val"":""x""
706+
}
707+
],
708+
""UserRoles"":[""authenticated""]
709+
}";
710+
711+
string secondGeneratedToken = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(secondHeader));
712+
713+
const string SESSION_CONFIG = $"session-context-config.{TestCategory.MSSQL}.json";
714+
RuntimeConfigProvider configProvider =
715+
TestHelper.GetRuntimeConfigProvider(TestHelper.GetRuntimeConfigLoader());
716+
RuntimeConfig config = configProvider.GetConfig();
717+
718+
RuntimeConfig updatedConfig = config with
719+
{
720+
DataSource = config.DataSource! with
721+
{
722+
Options = new Dictionary<string, object?>
723+
{
724+
// Matches MsSqlOptions.SetSessionContext (hyphenated naming policy).
725+
{ "set-session-context", true }
726+
}
727+
},
728+
Runtime = config.Runtime! with
729+
{
730+
Host = config.Runtime.Host! with
731+
{
732+
Authentication = new AuthenticationOptions(
733+
Provider: EasyAuthType.AppService.ToString(), Jwt: null)
734+
}
735+
}
736+
};
737+
738+
File.WriteAllText(SESSION_CONFIG, updatedConfig.ToJson());
739+
740+
string[] args = [$"--ConfigFileName={SESSION_CONFIG}"];
741+
using TestServer server = new(Program.CreateWebHostBuilder(args));
742+
using HttpClient client = server.CreateClient();
743+
744+
// Request with the first header
745+
HttpRequestMessage requestWithHeader = new(HttpMethod.Get, "api/Author");
746+
requestWithHeader.Headers.Add(AuthenticationOptions.CLIENT_PRINCIPAL_HEADER, firstGeneratedToken);
747+
requestWithHeader.Headers.Add(AuthorizationResolver.CLIENT_ROLE_HEADER, "authenticated");
748+
HttpResponseMessage responseWithHeader = await client.SendAsync(requestWithHeader);
749+
Assert.AreEqual(expected: HttpStatusCode.OK, actual: responseWithHeader.StatusCode);
750+
751+
// Request with second header
752+
HttpRequestMessage requestWithHeaderSec = new(HttpMethod.Get, "api/Author");
753+
requestWithHeaderSec.Headers.Add(AuthenticationOptions.CLIENT_PRINCIPAL_HEADER, secondGeneratedToken);
754+
requestWithHeaderSec.Headers.Add(AuthorizationResolver.CLIENT_ROLE_HEADER, "authenticated");
755+
HttpResponseMessage responseWithHeaderSec = await client.SendAsync(requestWithHeaderSec);
756+
Assert.AreEqual(expected: HttpStatusCode.OK, actual: responseWithHeaderSec.StatusCode);
757+
758+
HttpRequestMessage request = new(HttpMethod.Get, $"api/Author/id/10001");
759+
HttpResponseMessage response = await client.SendAsync(request);
760+
string responseBody = await response.Content.ReadAsStringAsync();
761+
762+
Assert.IsFalse(responseBody.Contains($"\"id\":10001"),
763+
"The GET request should not return the invalid row.");
764+
Assert.IsFalse(responseBody.Contains("Hidden Author"),
765+
"The GET request should not return any information related to the invalid row.");
766+
767+
if (File.Exists(SESSION_CONFIG))
768+
{
769+
File.Delete(SESSION_CONFIG);
770+
}
771+
}
772+
773+
#endregion
664774
}
665775
}

0 commit comments

Comments
 (0)