Skip to content

Commit 57382d5

Browse files
authored
[TestProxy] Common Sanitizers (#8038)
- Each recording or playback session now gets a modifiable list of sanitizers that are applied. - Session sanitizers are now cut at playback or record start. Any further session sanitizer addition will not directly affect the recording that has been started. - As a consequence of all these additional sanitizers, stored recordings will now be sanitized. The way this will work is - Client starts playback session, gets a recordingId back - Client uses recordingId to add a couple additional sanitizers - Client starts firing actual requests from the test - The proxy will sanitize the loaded recording according to which sanitizers are active when the first real request for the playback session occurs. - Upon calling /Admin/AddSanitizers or Admin/AddSanitizer, an object or list of objects representing the registered sanitizers is now returned to the calling client. These objects - contain the base sanitizer properties + the id of the registered sanitizer. - Added route /Admin/RemoveSanitizers which can apply to either the session-level or an individual playback/record session.
1 parent 4063a22 commit 57382d5

17 files changed

Lines changed: 1003 additions & 105 deletions

tools/test-proxy/Azure.Sdk.Tools.TestProxy.Tests/AdminTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1026,7 +1026,7 @@ public async Task RemoveSanitizerSucceedsForExistingSessionSanitizer()
10261026
};
10271027

10281028
var expectedSanitizerCount = testRecordingHandler.SanitizerRegistry.GetSanitizers().Count;
1029-
await controller.RemoveSanitizers(new RemoveSanitizerList() { Sanitizers = new List<string>() { "AZSDK001" } });
1029+
await controller.RemoveSanitizers(new RemoveSanitizerList() { Sanitizers = new List<string>() { "AZSDK0000" } });
10301030

10311031
httpContext.Response.Body.Seek(0, SeekOrigin.Begin);
10321032
var response = await JsonDocument.ParseAsync(httpContext.Response.Body, options: new JsonDocumentOptions() { AllowTrailingCommas = true });
@@ -1036,7 +1036,7 @@ public async Task RemoveSanitizerSucceedsForExistingSessionSanitizer()
10361036
var returnedSanitizerIds = TestHelpers.EnumerateArray<string>(prop);
10371037

10381038
Assert.Single(returnedSanitizerIds);
1039-
Assert.Equal("AZSDK001", returnedSanitizerIds.First());
1039+
Assert.Equal("AZSDK0000", returnedSanitizerIds.First());
10401040

10411041
Assert.Equal(expectedSanitizerCount - 1, testRecordingHandler.SanitizerRegistry.GetSanitizers().Count);
10421042
}

tools/test-proxy/Azure.Sdk.Tools.TestProxy.Tests/InfoTests.cs

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ namespace Azure.Sdk.Tools.TestProxy.Tests
1818
{
1919
public class InfoTests
2020
{
21+
private int DefaultExtensionCount { get { return new RecordingHandler(null).SanitizerRegistry.GetSanitizers().Count; } }
22+
2123
[Fact]
2224
public void TestReflectionModelBuild()
2325
{
@@ -79,17 +81,17 @@ public async Task TestReflectionModelWithTargetRecordSession()
7981
var model = new ActiveMetadataModel(testRecordingHandler, recordingId);
8082
var descriptions = model.Descriptions.ToList();
8183

82-
// we should have exactly 6 if we're counting all the customizations appropriately
83-
Assert.True(descriptions.Count == 6);
84+
// we should have exactly DefaultExtensionCount + 2 if we're counting all the customizations appropriately
85+
Assert.True(descriptions.Count == DefaultExtensionCount + 3);
8486
Assert.True(model.Matchers.Count() == 1);
85-
Assert.True(model.Sanitizers.Count() == 5);
87+
Assert.True(model.Sanitizers.Count() == DefaultExtensionCount + 2);
8688

8789
// confirm that the overridden matcher is showing up
88-
Assert.True(descriptions[3].ConstructorDetails.Arguments[1].Item2 == "\"ABC123\"");
89-
Assert.True(descriptions[4].ConstructorDetails.Arguments[1].Item2 == "\".+?\"");
90+
Assert.True(descriptions[DefaultExtensionCount].ConstructorDetails.Arguments[1].Item2 == "\"ABC123\"");
91+
Assert.True(descriptions[DefaultExtensionCount + 1].ConstructorDetails.Arguments[1].Item2 == "\".+?\"");
9092

9193
// and finally confirm our sanitizers are what we expect
92-
Assert.True(descriptions[5].Name == "CustomDefaultMatcher");
94+
Assert.True(descriptions[DefaultExtensionCount + 2].Name == "CustomDefaultMatcher");
9395
}
9496
}
9597
}

tools/test-proxy/Azure.Sdk.Tools.TestProxy.Tests/LoggingTests.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ public async Task PlaybackLogsSanitizedRequest()
5252
HttpResponse response = new DefaultHttpContext().Response;
5353
await testRecordingHandler.HandlePlaybackRequest(recordingId, request, response);
5454

55-
AssertLogs(logger, 4, 8);
55+
AssertLogs(logger, 4, 8, 12);
5656
}
5757
finally
5858
{
@@ -92,7 +92,7 @@ public async Task RecordingHandlerLogsSanitizedRequests()
9292

9393
try
9494
{
95-
AssertLogs(logger, 2, 8);
95+
AssertLogs(logger, 2, 8, 12);
9696
}
9797
finally
9898
{
@@ -101,7 +101,7 @@ public async Task RecordingHandlerLogsSanitizedRequests()
101101
}
102102
}
103103

104-
private static void AssertLogs(TestLogger logger, int offset, int expectedLength)
104+
private static void AssertLogs(TestLogger logger, int offset, int expectedLength, int expectedContentLength)
105105
{
106106
Assert.Equal(expectedLength, logger.Logs.Count);
107107
Assert.Equal(
@@ -116,10 +116,10 @@ private static void AssertLogs(TestLogger logger, int offset, int expectedLength
116116
Assert.Equal(
117117
"Request Body Content{\"key\":\"Location\",\"value\":\"https://fakeazsdktestaccount.table.core.windows.net/Tables\"}",
118118
logger.Logs[2 + offset].ToString());
119-
Assert.Equal("URI: [ https://fakeazsdktestaccount.table.core.windows.net/Tables]" +
119+
Assert.Equal("URI: [ https://Sanitized.table.core.windows.net/Tables]" +
120120
Environment.NewLine + "Headers: [{\"Accept\":[\"application/json;odata=minimalmetadata\"],\"Accept-Encoding\":[\"gzip, deflate\"],\"Authorization\":[\"Sanitized\"],\"Connection\":[\"keep-alive\"]," +
121-
"\"Content-Length\":[\"12\"],\"Content-Type\":[\"application/octet-stream\"],\"DataServiceVersion\":[\"3.0\"],\"Date\":[\"Tue, 18 May 2021 23:27:42 GMT\"]," +
122-
"\"User-Agent\":[\"azsdk-python-data-tables/12.0.0b7 Python/3.8.6 (Windows-10-10.0.19041-SP0)\"],\"x-ms-client-request-id\":[\"a4c24b7a-b830-11eb-a05e-10e7c6392c5a\"]," +
121+
"\"Content-Length\":[\"" + expectedContentLength + "\"],\"Content-Type\":[\"application/octet-stream\"],\"DataServiceVersion\":[\"3.0\"],\"Date\":[\"Tue, 18 May 2021 23:27:42 GMT\"]," +
122+
"\"User-Agent\":[\"azsdk-python-data-tables/12.0.0b7 Python/3.8.6 (Windows-10-10.0.19041-SP0)\"],\"x-ms-client-request-id\":[\"Sanitized\"]," +
123123
"\"x-ms-date\":[\"Tue, 18 May 2021 23:27:42 GMT\"],\"x-ms-version\":[\"2019-02-02\"]}]" + Environment.NewLine,
124124
logger.Logs[3 + offset].ToString());
125125
}

tools/test-proxy/Azure.Sdk.Tools.TestProxy.Tests/MatcherTests.cs

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using Azure.Sdk.Tools.TestProxy.Common;
1+
using Azure.Sdk.Tools.TestProxy.Common;
22
using Azure.Sdk.Tools.TestProxy.Matchers;
33
using Microsoft.AspNetCore.Http;
44
using Microsoft.AspNetCore.Http.Features;
@@ -299,6 +299,50 @@ public async Task CustomMatcherMatchesDifferentUriOrder()
299299
await testRecordingHandler.HandlePlaybackRequest(recordingId, playbackContext.Request, playbackContext.Response);
300300
Assert.Equal("WESTUS:20210909T204819Z:f9a33867-6efc-4748-b322-303b2b933466", playbackContext.Response.Headers["x-ms-routing-request-id"].ToString());
301301
}
302+
303+
304+
[Fact]
305+
public async Task EncodedUriAmpersandWorksCrossplat()
306+
{
307+
RecordingHandler testRecordingHandler = new RecordingHandler(Directory.GetCurrentDirectory());
308+
testRecordingHandler.Matcher = new CustomDefaultMatcher(ignoreQueryOrdering: true);
309+
var playbackContext = new DefaultHttpContext();
310+
var targetFile = "Test.RecordEntries/request_with_encoded_ampersand.json";
311+
var body = "{\"x-recording-file\":\"" + targetFile + "\"}";
312+
playbackContext.Request.Body = TestHelpers.GenerateStreamRequestBody(body);
313+
playbackContext.Request.ContentLength = body.Length;
314+
315+
var controller = new Playback(testRecordingHandler, new NullLoggerFactory())
316+
{
317+
ControllerContext = new ControllerContext()
318+
{
319+
HttpContext = playbackContext
320+
}
321+
};
322+
await controller.Start();
323+
var recordingId = playbackContext.Response.Headers["x-recording-id"].ToString();
324+
325+
playbackContext.Request.Headers.Clear();
326+
playbackContext.Response.Headers.Clear();
327+
playbackContext.Request.Method = "GET";
328+
329+
var requestHeaders = new Dictionary<string, string>(){
330+
{ "x-recording-id", recordingId },
331+
{ "x-recording-upstream-base-uri", "https://REDACTED" }
332+
};
333+
foreach (var kvp in requestHeaders)
334+
{
335+
playbackContext.Request.Headers.Add(kvp.Key, kvp.Value);
336+
}
337+
var queryString = "?api-version=1.0&year=2023&basinId=AL&govId=5";
338+
var path = "/weather/tropical/storms/json";
339+
playbackContext.Request.Host = new HostString("https://localhost:5001");
340+
playbackContext.Features.Get<IHttpRequestFeature>().RawTarget = path + queryString;
341+
await testRecordingHandler.HandlePlaybackRequest(recordingId, playbackContext.Request, playbackContext.Response);
342+
343+
Assert.Equal("Ref A: 980665086A12483993E2782EDFC9F29A Ref B: STBEDGE0106 Ref C: 2023-07-19T22:52:17Z", playbackContext.Response.Headers["X-MSEdge-Ref"].ToString());
344+
Assert.Equal(200, playbackContext.Response.StatusCode);
345+
}
302346
}
303347
}
304348

tools/test-proxy/Azure.Sdk.Tools.TestProxy.Tests/RecordingHandlerTests.cs

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ namespace Azure.Sdk.Tools.TestProxy.Tests
2929
{
3030
public class RecordingHandlerTests
3131
{
32+
private int DefaultExtensionCount { get { return new RecordingHandler(null).SanitizerRegistry.DefaultSanitizerList.Count; } }
33+
3234
#region helpers and private test fields
3335
private HttpContext GenerateHttpRequestContext(string[] headerValueStrings)
3436
{
@@ -97,11 +99,14 @@ private void _checkDefaultExtensions(RecordingHandler handlerForTest, CheckSkips
9799

98100
if (skipsToCheck.HasFlag(CheckSkips.IncludeSanitizers))
99101
{
100-
var sessionSanitizers = handlerForTest.SanitizerRegistry.GetSanitizers();
101-
Assert.Equal(3, sessionSanitizers.Count);
102-
Assert.IsType<RecordedTestSanitizer>(sessionSanitizers[0]);
103-
Assert.IsType<BodyKeySanitizer>(sessionSanitizers[1]);
104-
Assert.IsType<BodyKeySanitizer>(sessionSanitizers[2]);
102+
103+
var sanitizers = handlerForTest.SanitizerRegistry.GetSanitizers();
104+
105+
Assert.Equal(DefaultExtensionCount, sanitizers.Count);
106+
Assert.IsType<RecordedTestSanitizer>(sanitizers[0]);
107+
Assert.IsType<GeneralRegexSanitizer>(sanitizers[1]);
108+
Assert.IsType<GeneralRegexSanitizer>(sanitizers[2]);
109+
Assert.IsType<BodyKeySanitizer>(sanitizers[108]);
105110
}
106111
}
107112
#endregion
@@ -445,7 +450,7 @@ public async Task TestCanSkipRecordingEntireRequestResponse()
445450
var record = RecordSession.Deserialize(doc.RootElement);
446451
Assert.Single(record.Entries);
447452
var entry = record.Entries.First();
448-
Assert.Equal("value", JsonDocument.Parse(entry.Request.Body).RootElement.GetProperty("key").GetString());
453+
Assert.Equal("Sanitized", JsonDocument.Parse(entry.Request.Body).RootElement.GetProperty("key").GetString());
449454
Assert.Equal(MockHttpHandler.DefaultResponse, Encoding.UTF8.GetString(entry.Response.Body));
450455
}
451456
finally

tools/test-proxy/Azure.Sdk.Tools.TestProxy.Tests/SanitizerTests.cs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,33 @@ public void OauthResponseSanitizerCleansV2AuthRequest()
3636
Assert.Empty(session.Session.Entries);
3737
}
3838

39+
[Fact]
40+
public void SanitizerDecodesUnicodeAmpersandSanitizesClientIdAndSecret()
41+
{
42+
var session = TestHelpers.LoadRecordSession("Test.RecordEntries/request_with_encoding.json");
43+
44+
var clientSan = new BodyRegexSanitizer(regex: "(client_id=)(?<cid>[^&\\\"]+)", groupForReplace: "cid");
45+
var secretSan = new BodyRegexSanitizer(regex: "client_secret=(?<secret>[^&\\\"]+)", groupForReplace: "secret");
46+
47+
session.Session.Sanitize(clientSan);
48+
session.Session.Sanitize(secretSan);
49+
50+
Assert.Equal("client_id=Sanitized&grant_type=client_credentials&client_info=1&client_secret=Sanitized&claims=%7B%22access_token=blahblah", Encoding.UTF8.GetString(session.Session.Entries[0].Request.Body));
51+
}
52+
53+
[Fact]
54+
public void EnsureSASCleanupDoesntOverrunInXML()
55+
{
56+
var sanitizerDictionary = new SanitizerDictionary();
57+
var sessionwithXmlBody = TestHelpers.LoadRecordSession("Test.RecordEntries/xml_body_with_sas_present.json");
58+
59+
Assert.True(sanitizerDictionary.Sanitizers.TryGetValue("AZSDK1007", out RegisteredSanitizer SASURISanitizer));
60+
61+
sessionwithXmlBody.Session.Sanitize(SASURISanitizer.Sanitizer);
62+
63+
Assert.Contains("<CopyProgress>1024/1024</CopyProgress>", Encoding.UTF8.GetString(sessionwithXmlBody.Session.Entries[0].Response.Body));
64+
}
65+
3966
[Fact]
4067
public void OauthResponseSanitizerCleansNonV2AuthRequest()
4168
{
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
{
2+
"Entries": [
3+
{
4+
"RequestUri": "https://REDACTED/weather/tropical/storms/json?api-version=1.0\u0026year=2023\u0026basinId=AL\u0026govId=5",
5+
"RequestMethod": "GET",
6+
"RequestHeaders": {
7+
},
8+
"RequestBody": null,
9+
"StatusCode": 200,
10+
"ResponseHeaders": {
11+
"Cache-Control": "public, max-age=600",
12+
"Content-Length": "123",
13+
"Content-Type": "application/json; charset=utf-8",
14+
"Date": "Wed, 19 Jul 2023 22:52:17 GMT",
15+
"Expires": "Wed, 19 Jul 2023 23:02:17 GMT",
16+
"Strict-Transport-Security": "max-age=31536000; includeSubDomains",
17+
"Vary": "Accept-Encoding",
18+
"X-Cache": "CONFIG_NOCACHE",
19+
"X-Content-Type-Options": "nosniff",
20+
"x-ms-azuremaps-region": "West US 2",
21+
"X-MSEdge-Ref": "Ref A: 980665086A12483993E2782EDFC9F29A Ref B: STBEDGE0106 Ref C: 2023-07-19T22:52:17Z"
22+
},
23+
"ResponseBody": {
24+
"results": [
25+
{
26+
"year": "2023",
27+
"basinId": "AL",
28+
"govId": 5,
29+
"name": "Don",
30+
"isActive": true,
31+
"isRetired": false,
32+
"isSubtropical": false
33+
}
34+
]
35+
}
36+
}
37+
],
38+
"Variables": {}
39+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
{
2+
"Entries": [
3+
{
4+
"RequestUri": "https://login.microsoftonline.com/00000000-0000-0000-0000-000000000000/oauth2/v2.0/token",
5+
"RequestMethod": "POST",
6+
"RequestHeaders": {
7+
"Accept": "application/json",
8+
"Accept-Encoding": "gzip, deflate",
9+
"client-request-id": "76ea37cc-3d67-45ec-9bf5-9f7c0cb8400e",
10+
"Connection": "keep-alive",
11+
"Content-Length": "284",
12+
"Content-Type": "application/x-www-form-urlencoded",
13+
"Cookie": "fpc=gibberishblahblah; stsservicecookie=estsfd; x-ms-gateway-slice=estsfd",
14+
"User-Agent": "azsdk-python-identity/1.11.0b4 Python/3.8.6 (Windows-10-10.0.22000-SP0)",
15+
"x-client-cpu": "x64",
16+
"x-client-current-telemetry": "4|730,0|",
17+
"x-client-last-telemetry": "4|0|||",
18+
"x-client-os": "win32",
19+
"x-client-sku": "MSAL.Python",
20+
"x-client-ver": "1.18.0",
21+
"x-ms-lib-capability": "retry-after, h429"
22+
},
23+
"RequestBody": "client_id=00000000-0000-0000-0000-000000000000\u0026grant_type=client_credentials\u0026client_info=1\u0026client_secret=a_secret~value_\u0026claims=%7B%22access_token=blahblah",
24+
"StatusCode": 200,
25+
"ResponseHeaders": {
26+
"Cache-Control": "no-store, no-cache",
27+
"client-request-id": "76ea37cc-3d67-45ec-9bf5-9f7c0cb8400e",
28+
"Content-Length": "111",
29+
"Content-Type": "application/json; charset=utf-8",
30+
"Date": "Wed, 31 Aug 2022 21:01:03 GMT",
31+
"Expires": "-1",
32+
"P3P": "CP=\u0022DSP CUR OTPi IND OTRi ONL FIN\u0022",
33+
"Pragma": "no-cache",
34+
"Set-Cookie": [
35+
"fpc=gibberishblahblah; expires=Fri, 30-Sep-2022 21:01:04 GMT; path=/; secure; HttpOnly; SameSite=None",
36+
"x-ms-gateway-slice=estsfd; path=/; secure; samesite=none; httponly",
37+
"stsservicecookie=estsfd; path=/; secure; samesite=none; httponly"
38+
],
39+
"Strict-Transport-Security": "max-age=31536000; includeSubDomains",
40+
"X-Content-Type-Options": "nosniff",
41+
"x-ms-clitelem": "1,0,0,,",
42+
"x-ms-ests-server": "2.1.13622.4 - WUS2 ProdSlices",
43+
"X-XSS-Protection": "0"
44+
},
45+
"ResponseBody": {
46+
"token_type": "Bearer",
47+
"expires_in": 86399,
48+
"ext_expires_in": 86399,
49+
"refresh_in": 43199,
50+
"access_token": "Sanitized"
51+
}
52+
}
53+
]
54+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"Entries": [
3+
{
4+
"RequestUri": "https://REDACTED.blob.core.windows.net/blockblobclienttestsynccopyfromuri?comp=list&include=copy&prefix=destS72lXCxNKX&restype=container",
5+
"RequestMethod": "GET",
6+
"RequestHeaders": {
7+
"Authorization": "Sanitized",
8+
"User-Agent": "azsdk-cpp-storage-blobs/12.11.0-beta.2 (Windows 10 Enterprise 6.3 22631 22621.1.amd64fre.ni_release.220506-1250)",
9+
"x-ms-client-request-id": "Sanitized",
10+
"x-ms-date": "Sun, 28 Apr 2024 06:31:46 GMT",
11+
"x-ms-version": "2023-11-03"
12+
},
13+
"RequestBody": null,
14+
"StatusCode": 200,
15+
"ResponseHeaders": {
16+
"Content-Type": "application/xml",
17+
"Date": "Sun, 28 Apr 2024 06:31:45 GMT",
18+
"Server": [
19+
"Windows-Azure-Blob/1.0",
20+
"Microsoft-HTTPAPI/2.0"
21+
],
22+
"Transfer-Encoding": "chunked",
23+
"x-ms-client-request-id": "Sanitized",
24+
"x-ms-request-id": "Sanitized",
25+
"x-ms-version": "2023-11-03"
26+
},
27+
"ResponseBody": "\uFEFF<?xml version=\"1.0\" encoding=\"utf-8\"?><EnumerationResults ServiceEndpoint=\"https://afakeblobname.blob.core.windows.net/\" ContainerName=\"blockblobclienttestsynccopyfromuri\"><Prefix>destS72lXCxNKX</Prefix><Blobs><Blob><Name>destS72lXCxNKX</Name><VersionId>2024-04-28T06:31:45.7176515Z</VersionId><IsCurrentVersion>true</IsCurrentVersion><Properties><Creation-Time>Sun, 28 Apr 2024 06:31:45 GMT</Creation-Time><Last-Modified>Sun, 28 Apr 2024 06:31:45 GMT</Last-Modified><Etag>0x8DC674CE03A6C6F</Etag><Content-Length>1024</Content-Length><Content-Type>application/octet-stream</Content-Type><Content-Encoding /><Content-Language /><Content-CRC64>iu0B5sKKllE=</Content-CRC64><Content-MD5>7Fnk5RibvrmdtnSTCz3FWA==</Content-MD5><Cache-Control /><Content-Disposition /><BlobType>BlockBlob</BlobType><AccessTier>Hot</AccessTier><AccessTierInferred>true</AccessTierInferred><LeaseStatus>unlocked</LeaseStatus><LeaseState>available</LeaseState><CopyId>d49efb17-b054-4515-aac3-6e301ee771ce</CopyId><CopySource>https://afakeblobname.blob.core.windows.net:443/blockblobclienttestsynccopyfromuri/sourceC4GbldEmXi?se=2024-04-19T09%3a42%3a44Z&amp;sp=xxx&amp;spr=https%2chttp&amp;sr=c&amp;sv=2023-11-03</CopySource><CopyStatus>success</CopyStatus><CopyProgress>1024/1024</CopyProgress><CopyCompletionTime>Fri, 26 Apr 2024 09:42:44 GMT</CopyCompletionTime><ServerEncrypted>true</ServerEncrypted></Properties><OrMetadata /></Blob></Blobs><NextMarker /></EnumerationResults>"
28+
}
29+
]
30+
}

0 commit comments

Comments
 (0)