tools/test-proxy/Azure.Sdk.Tools.TestProxy/Admin.cs (258 lines of code) (raw):

// 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 Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Text.Json; using System.Threading.Tasks; namespace Azure.Sdk.Tools.TestProxy { [ApiController] [Route("[controller]/[action]")] public sealed class Admin : ControllerBase { private readonly RecordingHandler _recordingHandler; private readonly ILogger _logger; public Admin(RecordingHandler recordingHandler, ILoggerFactory loggingFactory) { _recordingHandler = recordingHandler; _logger = loggingFactory.CreateLogger<Admin>(); } [HttpPost] public async Task Reset() { DebugLogger.LogAdminRequestDetails(_logger, Request); var recordingId = RecordingHandler.GetHeader(Request, "x-recording-id", allowNulls: true); await _recordingHandler.SetDefaultExtensions(recordingId); } [HttpGet] public void IsAlive() { DebugLogger.LogAdminRequestDetails(_logger, Request); Response.StatusCode = 200; } [HttpPost] public async Task AddTransform() { DebugLogger.LogAdminRequestDetails(_logger, Request); var tName = RecordingHandler.GetHeader(Request, "x-abstraction-identifier"); var recordingId = RecordingHandler.GetHeader(Request, "x-recording-id", allowNulls: true); ResponseTransform t = (ResponseTransform)GetTransform(tName, await HttpRequestInteractions.GetBody(Request)); if (recordingId != null) { _recordingHandler.AddTransformToRecording(recordingId, t); } else { _recordingHandler.Transforms.Add(t); } } [HttpPost] public async Task RemoveSanitizers() { DebugLogger.LogAdminRequestDetails(_logger, Request); // Originally, this list was parsed using [FromBody], which was implicitly case insensitive. Need to maintain for compat. var sanitizerList = await HttpRequestInteractions.GetBody<RemoveSanitizerList>(Request, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); var recordingId = RecordingHandler.GetHeader(Request, "x-recording-id", allowNulls: true); var removedSanitizers = new List<string>(); // - body may be empty // - body may actually pass an empty list. handle both. if ((sanitizerList?.Sanitizers ?? new List<string>()).Count == 0) { throw new HttpException(HttpStatusCode.BadRequest, "At least one sanitizerId for removal must be provided."); } foreach(var sanitizerId in sanitizerList.Sanitizers) { var removedId = await _recordingHandler.UnregisterSanitizer(sanitizerId, recordingId); if (!string.IsNullOrWhiteSpace(removedId)) { removedSanitizers.Add(sanitizerId); } } var json = JsonSerializer.Serialize(new { Removed = removedSanitizers }); Response.ContentType = "application/json"; Response.ContentLength = json.Length; await Response.WriteAsync(json); } [HttpGet] public async Task GetSanitizers() { DebugLogger.LogAdminRequestDetails(_logger, Request); var recordingId = RecordingHandler.GetHeader(Request, "x-recording-id", allowNulls: true); List<RegisteredSanitizer> sanitizers; if (!string.IsNullOrEmpty(recordingId)) { var session = _recordingHandler.GetActiveSession(recordingId); sanitizers = await _recordingHandler.SanitizerRegistry.GetRegisteredSanitizers(session); } else { sanitizers = await _recordingHandler.SanitizerRegistry.GetRegisteredSanitizers(); } var json = JsonSerializer.Serialize(new { Sanitizers = sanitizers }); Response.ContentType = "application/json"; Response.ContentLength = json.Length; await Response.WriteAsync(json); } [HttpPost] public async Task AddSanitizer() { DebugLogger.LogAdminRequestDetails(_logger, Request); var sName = RecordingHandler.GetHeader(Request, "x-abstraction-identifier"); var recordingId = RecordingHandler.GetHeader(Request, "x-recording-id", allowNulls: true); RecordedTestSanitizer s = (RecordedTestSanitizer)GetSanitizer(sName, await HttpRequestInteractions.GetBody(Request)); string registeredSanitizerId; if (recordingId != null) { registeredSanitizerId = await _recordingHandler.RegisterSanitizer(s, recordingId); } else { registeredSanitizerId = await _recordingHandler.RegisterSanitizer(s); } var json = JsonSerializer.Serialize(new { Sanitizer = registeredSanitizerId }); Response.ContentType = "application/json"; Response.ContentLength = json.Length; await Response.WriteAsync(json); } [HttpPost] public async Task AddSanitizers() { DebugLogger.LogAdminRequestDetails(_logger, Request); var recordingId = RecordingHandler.GetHeader(Request, "x-recording-id", allowNulls: true); // parse all of them first, any exceptions should pop here var workload = (await HttpRequestInteractions.GetBody<List<SanitizerBody>>(Request)).Select(s => (RecordedTestSanitizer)GetSanitizer(s.Name, s.Body)).ToList(); if (workload.Count == 0) { throw new HttpException(HttpStatusCode.BadRequest, "When bulk adding sanitizers, ensure there is at least one sanitizer added in each batch. Received 0 work items."); } // 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. var registeredSanitizers = await _recordingHandler.RegisterSanitizers(workload, recordingId); if (recordingId != null) { Response.Headers.Append("x-recording-id", recordingId); } var json = JsonSerializer.Serialize(new { Sanitizers = registeredSanitizers }); Response.ContentType = "application/json"; Response.ContentLength = json.Length; await Response.WriteAsync(json); } [HttpPost] public async Task SetMatcher() { DebugLogger.LogAdminRequestDetails(_logger, Request); var mName = RecordingHandler.GetHeader(Request, "x-abstraction-identifier"); var recordingId = RecordingHandler.GetHeader(Request, "x-recording-id", allowNulls: true); RecordMatcher m = (RecordMatcher)GetMatcher(mName, await HttpRequestInteractions.GetBody(Request)); if (recordingId != null) { _recordingHandler.SetMatcherForRecording(recordingId, m); } else { _recordingHandler.Matcher = m; } } [HttpPost] public async Task SetRecordingOptions() { DebugLogger.LogAdminRequestDetails(_logger, Request); var options = await HttpRequestInteractions.GetBody<Dictionary<string, object>>(Request); var recordingId = RecordingHandler.GetHeader(Request, "x-recording-id", allowNulls: true); _recordingHandler.SetRecordingOptions(options, recordingId); } public object GetSanitizer(string name, JsonDocument body) { return GenerateInstance("Azure.Sdk.Tools.TestProxy.Sanitizers.", name, new HashSet<string>() { "value" }, documentBody: body); } public object GetTransform(string name, JsonDocument body) { return GenerateInstance("Azure.Sdk.Tools.TestProxy.Transforms.", name, new HashSet<string>() { }, documentBody: body); } public object GetMatcher(string name, JsonDocument body) { return GenerateInstance("Azure.Sdk.Tools.TestProxy.Matchers.", name, new HashSet<string>() { }, documentBody:body); } private object GenerateInstance(string typePrefix, string name, HashSet<string> acceptableEmptyArgs, JsonDocument documentBody = null) { Type t = Type.GetType(typePrefix + name); if (t == null) { throw new HttpException(HttpStatusCode.BadRequest, String.Format("Requested type {0} is not not recognized.", typePrefix + name)); } var arg_list = new List<Object> { }; // we are deliberately assuming here that there will only be a single constructor var ctor = t.GetConstructors()[0]; var paramsSet = ctor.GetParameters(); // walk across our constructor params. check inside the body for a resulting value for each of them foreach (var param in paramsSet) { if (documentBody != null && documentBody.RootElement.TryGetProperty(param.Name, out var jsonElement)) { if (DebugLogger.CheckLogLevel(LogLevel.Debug)) { _logger.LogDebug("Request Body Content" + JsonSerializer.Serialize(documentBody.RootElement)); } object argumentValue = null; switch (jsonElement.ValueKind) { case JsonValueKind.Null: case JsonValueKind.String: argumentValue = jsonElement.GetString(); break; case JsonValueKind.True: case JsonValueKind.False: argumentValue = jsonElement.GetBoolean(); break; case JsonValueKind.Object: try { argumentValue = Activator.CreateInstance(param.ParameterType, new List<object> { jsonElement }.ToArray()); } catch (Exception e) { if (e.InnerException is HttpException) { throw e.InnerException; } else throw; } break; default: throw new HttpException(HttpStatusCode.BadRequest, $"{jsonElement.ValueKind} parameters are not supported"); } if(argumentValue == null || (argumentValue is string stringResult && string.IsNullOrEmpty(stringResult))) { if (!acceptableEmptyArgs.Contains(param.Name)) { throw new HttpException(HttpStatusCode.BadRequest, $"Parameter \"{param.Name}\" was passed with no value. Please check the request body and try again."); } } arg_list.Add((object)argumentValue); } else { if (param.IsOptional) { arg_list.Add(param.DefaultValue); } else { throw new HttpException(HttpStatusCode.BadRequest, $"Required parameter key \"{param.Name}\" was not found in the request body."); } } } try { return Activator.CreateInstance(t, arg_list.ToArray()); } catch(Exception e) { if (e.InnerException is HttpException) { throw e.InnerException; } else throw; } } } }