Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add proxy audit log and other assorted fixes #8781

Merged
merged 20 commits into from
Aug 6, 2024
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
d420268
allow the sanitizers to clear
scbedd Aug 2, 2024
8d4d221
remove a test that doesn't have a point if we don't error on sanitize…
scbedd Aug 2, 2024
c57ce50
use a unified lock for all access to a recording. this should enforce…
scbedd Aug 2, 2024
cdf3030
Creating an audit log so we can see raw associated information for im…
semick-dev Aug 4, 2024
8bcc5a2
a couple more audit items!
semick-dev Aug 4, 2024
ff853ea
fix the issue with missing audit entry
semick-dev Aug 4, 2024
fb3aed7
still logging the wrong thing
semick-dev Aug 4, 2024
0ee3567
we need to add a bit more details
semick-dev Aug 4, 2024
c1e7435
oh my gosh! I was using the session sanitizer lock ACROSS THE BOARD
semick-dev Aug 4, 2024
53bf069
just cleaning up some using
semick-dev Aug 4, 2024
75334ff
ensure that we can't stop playback while streaming a response back
semick-dev Aug 5, 2024
66159a9
whoopsy. missed an await
semick-dev Aug 5, 2024
a0f6a71
correct a param
semick-dev Aug 5, 2024
824573a
why is a clean and rebuild not turning up these simple fixes?
semick-dev Aug 5, 2024
fc4b3a6
reduce the complexity of the cleanup for inmemory sessions
scbedd Aug 5, 2024
0a95dd7
simply the audit URL. add a couple additional logging items so that w…
scbedd Aug 5, 2024
f2729d7
update launchSettings to not be different from main
scbedd Aug 5, 2024
1ae3a9f
simplify audit controller even further
scbedd Aug 5, 2024
1295de8
add a couple test cases, ensure that we add a recording session to th…
scbedd Aug 5, 2024
d03340a
Update tools/test-proxy/Azure.Sdk.Tools.TestProxy/RecordingHandler.cs
scbedd Aug 6, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 3 additions & 31 deletions tools/test-proxy/Azure.Sdk.Tools.TestProxy.Tests/AdminTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -939,33 +939,7 @@ public async Task TestAddSanitizerContinuesWithTwoRequiredParams()
}

[Fact]
public async Task RemoveSanitizerErrorsForInvalidIdOnRecording()
{
RecordingHandler testRecordingHandler = new RecordingHandler(Directory.GetCurrentDirectory());
var httpContext = new DefaultHttpContext();
await testRecordingHandler.StartPlaybackAsync("Test.RecordEntries/oauth_request_with_variables.json", httpContext.Response);
var recordingId = httpContext.Response.Headers["x-recording-id"];
httpContext.Request.Headers["x-recording-id"] = recordingId;
httpContext.Response.Body = new MemoryStream();
var controller = new Admin(testRecordingHandler, _nullLogger)
{
ControllerContext = new ControllerContext()
{
HttpContext = httpContext
}
};

var testSet = new RemoveSanitizerList() { Sanitizers = new List<string>() { "0" } };

var assertion = await Assert.ThrowsAsync<HttpException>(
async () => await controller.RemoveSanitizers(testSet)
);

Assert.Contains("Unable to remove 1 sanitizer. Detailed list follows: \nThe requested sanitizer for removal \"0\" is not active on recording/playback with id", assertion.Message);
}

[Fact]
public async Task RemoveSanitizerErrorsForInvalidId()
public async Task RemoveSanitizersSilentlyAcceptsInvalidSanitizer()
{
RecordingHandler testRecordingHandler = new RecordingHandler(Directory.GetCurrentDirectory());
var httpContext = new DefaultHttpContext();
Expand All @@ -980,11 +954,9 @@ public async Task RemoveSanitizerErrorsForInvalidId()

var testSet = new RemoveSanitizerList() { Sanitizers = new List<string>() { "AZSDK00-1" } };

var assertion = await Assert.ThrowsAsync<HttpException>(
async () => await controller.RemoveSanitizers(testSet)
);
await controller.RemoveSanitizers(testSet);

Assert.Equal("Unable to remove 1 sanitizer. Detailed list follows: \nThe requested sanitizer for removal \"AZSDK00-1\" is not active at the session level.", assertion.Message);
Assert.Equal(200, httpContext.Response.StatusCode);
}

[Fact]
Expand Down
44 changes: 10 additions & 34 deletions tools/test-proxy/Azure.Sdk.Tools.TestProxy/Admin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,39 +70,26 @@ public async Task RemoveSanitizers([FromBody]RemoveSanitizerList sanitizerList)
var recordingId = RecordingHandler.GetHeader(Request, "x-recording-id", allowNulls: true);

var removedSanitizers = new List<string>();
var exceptionsList = new List<string>();

if (sanitizerList.Sanitizers.Count == 0)
{
throw new HttpException(HttpStatusCode.BadRequest, "At least one sanitizerId for removal must be provided.");
}

foreach(var sanitizerId in sanitizerList.Sanitizers) {
try
var removedId = await _recordingHandler.UnregisterSanitizer(sanitizerId, recordingId);
if (!string.IsNullOrWhiteSpace(removedId))
{
var removedId = await _recordingHandler.UnregisterSanitizer(sanitizerId, recordingId);
removedSanitizers.Add(sanitizerId);
}
catch (HttpException ex) {
exceptionsList.Add(ex.Message);
}
}

if (exceptionsList.Count > 0)
{
var varExceptionMessage = $"Unable to remove {exceptionsList.Count} sanitizer{(exceptionsList.Count > 1 ? 's' : string.Empty)}. Detailed list follows: \n"
+ string.Join("\n", exceptionsList);
throw new HttpException(HttpStatusCode.BadRequest, varExceptionMessage);
}
else
{
var json = JsonSerializer.Serialize(new { Removed = removedSanitizers });
var json = JsonSerializer.Serialize(new { Removed = removedSanitizers });

Response.ContentType = "application/json";
Response.ContentLength = json.Length;
Response.ContentType = "application/json";
Response.ContentLength = json.Length;

await Response.WriteAsync(json);
}
await Response.WriteAsync(json);
}

[HttpGet]
Expand Down Expand Up @@ -171,22 +158,11 @@ public async Task AddSanitizers()
throw new HttpException(HttpStatusCode.BadRequest, "When bulk adding sanitizers, ensure there is at least one sanitizer added in each batch. Received 0 work items.");
}

var registeredSanitizers = new List<string>();
// we need check if a recording id is present BEFORE the loop, as we want to encapsulate the entire
// sanitizer add operation in a single lock, rather than gathering and releasing a sanitizer lock
// for the session/recording on _each_ sanitizer addition.

// register them all
foreach(var sanitizer in workload)
{
if (recordingId != null)
{
var registeredId = await _recordingHandler.RegisterSanitizer(sanitizer, recordingId);
registeredSanitizers.Add(registeredId);
}
else
{
var registeredId = await _recordingHandler.RegisterSanitizer(sanitizer);
registeredSanitizers.Add(registeredId);
}
}
var registeredSanitizers = await _recordingHandler.RegisterSanitizers(workload, recordingId);

if (recordingId != null)
{
Expand Down
93 changes: 93 additions & 0 deletions tools/test-proxy/Azure.Sdk.Tools.TestProxy/Audit.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using Azure.Sdk.Tools.TestProxy.Common;
using Azure.Sdk.Tools.TestProxy.Common.Exceptions;
using Azure.Sdk.Tools.TestProxy.Store;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Net;
using System.Text;
using System.Threading.Tasks;

namespace Azure.Sdk.Tools.TestProxy
{
[ApiController]
[Route("[controller]/[action]")]
public sealed class Audit : ControllerBase
{
private readonly ILogger _logger;
private readonly RecordingHandler _recordingHandler;

public Audit(RecordingHandler recordingHandler, ILoggerFactory loggerFactory)
{
_recordingHandler = recordingHandler;
_logger = loggerFactory.CreateLogger<Record>();
}

[HttpGet]
public async Task GetAuditLogs()
{

var allAuditSessions = _recordingHandler.RetrieveOngoingAuditLogs();
allAuditSessions.AddRange(_recordingHandler.AuditSessions.Values);

StringBuilder stringBuilder = new StringBuilder();

foreach (var auditLogQueue in allAuditSessions) {
while (auditLogQueue.TryDequeue(out var logItem))
{
stringBuilder.Append(logItem.ToCsvString() + Environment.NewLine);
scbedd marked this conversation as resolved.
Show resolved Hide resolved
}
}

Response.ContentType = "text/plain";

await Response.WriteAsync(stringBuilder.ToString());
}


[HttpPost]
public async Task Push([FromBody()] IDictionary<string, object> options = null)
{
DebugLogger.LogAdminRequestDetails(_logger, Request);

var pathToAssets = RecordingHandler.GetAssetsJsonLocation(StoreResolver.ParseAssetsJsonBody(options), _recordingHandler.ContextDirectory);

await _recordingHandler.Store.Push(pathToAssets);
}

[HttpPost]
[AllowEmptyBody]
public async Task Stop([FromBody()] IDictionary<string, string> variables = null)
scbedd marked this conversation as resolved.
Show resolved Hide resolved
{
string id = RecordingHandler.GetHeader(Request, "x-recording-id");
bool save = true;
EntryRecordMode mode = RecordingHandler.GetRecordMode(Request);

if (mode != EntryRecordMode.Record && mode != EntryRecordMode.DontRecord)
{
throw new HttpException(HttpStatusCode.BadRequest, "When stopping a recording and providing a \"x-recording-skip\" value, only value \"request-response\" is accepted.");
}

if (mode == EntryRecordMode.DontRecord)
{
save = false;
}

DebugLogger.LogAdminRequestDetails(_logger, Request);

await _recordingHandler.StopRecording(id, variables: variables, saveRecording: save);
}

public async Task HandleRequest()
{
string id = RecordingHandler.GetHeader(Request, "x-recording-id");

await _recordingHandler.HandleRecordRequestAsync(id, Request, Response);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using System;

namespace Azure.Sdk.Tools.TestProxy.Common
{
public class AuditLogItem
{
public string RecordingId { get; set; }

public DateTime Timestamp { get; set; }

public string Uri { get; set; }

public string Verb { get; set; }

public string Message { get; set; }

public AuditLogItem(string recordingId, string requestUri, string requestMethod) {
RecordingId = recordingId;
Timestamp = DateTime.UtcNow;

Uri = requestUri;
Verb = requestMethod;
}

public string ToCsvString()
{
return $"{RecordingId},{Timestamp.ToString("o")},{Verb},{Uri},{Message}";
}

public AuditLogItem(string recordingId, string message)
{
RecordingId = recordingId;
Timestamp = DateTime.UtcNow;

Message = message;
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Collections.Generic;
using System.Text.Json;
using Microsoft.AspNetCore.Http.Extensions;
using System.Collections.Concurrent;

namespace Azure.Sdk.Tools.TestProxy.Common
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
Expand All @@ -9,13 +10,13 @@ namespace Azure.Sdk.Tools.TestProxy.Common
{
public class ModifiableRecordSession
{
public RecordMatcher CustomMatcher { get; set;}
public RecordMatcher CustomMatcher { get; set; }

public RecordSession Session { get; }

public ModifiableRecordSession(SanitizerDictionary sanitizerRegistry, string sessionId)
{
lock(sanitizerRegistry.SessionSanitizerLock)
lock (sanitizerRegistry.SessionSanitizerLock)
{
this.AppliedSanitizers = sanitizerRegistry.SessionSanitizers.ToList();
}
Expand All @@ -42,18 +43,17 @@ public ModifiableRecordSession(RecordSession session, SanitizerDictionary saniti

public List<ResponseTransform> AdditionalTransforms { get; } = new List<ResponseTransform>();

public SemaphoreSlim SanitizerLock = new SemaphoreSlim(1);

public List<string> AppliedSanitizers { get; set; } = new List<string>();
public List<string> ForRemoval { get; } = new List<string>();

public string SourceRecordingId { get; set; }

public int PlaybackResponseTime { get; set; }

public ConcurrentQueue<AuditLogItem> AuditLog { get; set; } = new ConcurrentQueue<AuditLogItem>();
public async void ResetExtensions(SanitizerDictionary sanitizerDictionary)
{
await SanitizerLock.WaitAsync();
await Session.EntryLock.WaitAsync();
try
{
AdditionalTransforms.Clear();
Expand All @@ -66,7 +66,7 @@ public async void ResetExtensions(SanitizerDictionary sanitizerDictionary)
}
finally
{
SanitizerLock.Release();
Session.EntryLock.Release();
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
Expand Down
Loading
Loading