From 19fdb7c12a87c28d4a2ee18199db2ad01d1aa0dd Mon Sep 17 00:00:00 2001 From: Maggie Kimani Date: Wed, 23 Jun 2021 11:55:33 +0300 Subject: [PATCH 01/28] Merges dev into master (#617) * Add telemetry diagnostic tracing for samples * Add trace info for permissions * Update telemetry trace capture points * Install Application insights for trace telemetry tracking * Adds trace messages to log diagnostic events at service level * Track exceptions including stack traces at controller level * Add Telemetry lib. to OpenAPIService * Add OpenApiService as Singleton * Use static property to set TelemetryClient * Use parameter to set telemetry client in OpenApiService * Adds the ChangesService class as a singleton on startup and refactor code * Modify the ChangesService from a static class to a singleton and add trace telemetry for tracking purposes * Update value of the trace properties dictionary * Initialize a telemetry client and use it to track exceptions * Use a static property to set the telemetry client and use it to track trace telemetry * Install ApplicationInsights * Revert "Adds the ChangesService class as a singleton on startup and refactor code" This reverts commit 36bb74af07c2dd5f7351286860a033daab59730d. * Revert "Modify the ChangesService from a static class to a singleton and add trace telemetry for tracking purposes" This reverts commit 71eeb6b9e659c9e2e60af1de0876cc668953d337. * Reverts changes for ChangesService class and adds trace telemetry * Edit value for the trace properties dictionary * Remove unnecessary line * Remove whitespaces * Add null check * Track exceptions thrown for better inspection * Update OpenApiService and controller trace information and properties * Message update * Rename the trace properties dictionaries * Further trace telemetry message and property updates * Add log messages to track trace events and exceptions * Install Application Insights * Edit the trace message * Add count properties and prevent them from being sanitized * Minor edit * Create UtilityConstants class that will have constants * Add UtilityService project dependencies * Use UtilityService constants * Add TelemetryClientUtility class and update its implementations Also, some class renames and code simplifications. Remove unnecessary code as well. * Code formatting * Transfer TelemetryClientUtility to a dedicated library; delete unnecessary packages and code * Remove unnecessary code and imports; code simplification * Auto stash before merge of "feature/mk/instrument-changes-service" and "origin/feature/is/code-instrumentation" * Add a reference to the TelemetryClientWrapper * Add a reference for the TelemetryClientWrapper * Use a singleton instance of telemetry client to track telemetry across multiple services and controllers * Add a Changes property key for storing count data to avoid unnecessary numerical sanitization * Clean up file * Add a trace message to trigger an alert when our permission data source is empty * Fix indentation * Remove whitespace * Create a joining service. * Refactor startup file * Inject IServiceProvider and use it to get an IPermissionsStore instance * Use the TelemetryClient available from our DI container to track telemetry * Remove optional param * Add/remove packages * Mock an IServiceProvider object and use it to call a mocked instance of IPermissionsStore * Simplify null check * Remove files * Create interfaces and initialize the services as singletons * Implement the interface * Add OpenApi telemetry property key as a constant * Replace string with changes telemetry property key constant * Add project reference * Code cleanup * Add property to prevent graph uri Version from sanitization * Add abstract methods to interfaces and refactor code * Code cleanup * Rename dictionary value to PermissionsStore and delete the dictionary afterwards * Rename dictionary value * Use nameof() operator to get the class names * Use defensive programming Co-authored-by: Irvine Sunday Co-authored-by: Vincent Biret Co-authored-by: George --- .../ChangeLogRecordsModelShould.cs | 12 +- ChangesService.Test/ChangesServiceShould.cs | 49 +++--- ChangesService.Test/ChangesStoreShould.cs | 15 +- ChangesService/ChangesService.csproj | 2 + ChangesService/Interfaces/IChangesService.cs | 19 +++ ChangesService/Services/ChangesService.cs | 45 +++++- ChangesService/Services/ChangesStore.cs | 39 ++++- .../CodeSnippetsReflection.csproj | 5 + CodeSnippetsReflection/SnippetsGenerator.cs | 16 +- .../Services/PermissionsStore.cs | 102 ++++++++++++- .../GraphExplorerSamplesService.csproj | 2 + .../Services/SamplesStore.cs | 92 ++++++++---- GraphWebApi/Controllers/ChangesController.cs | 46 ++++-- .../GraphExplorerPermissionsController.cs | 21 ++- .../GraphExplorerSamplesController.cs | 48 +++++- .../GraphExplorerSnippetsController.cs | 31 +++- GraphWebApi/Controllers/OpenApiController.cs | 95 ++++++++---- GraphWebApi/GraphWebApi.csproj | 3 +- GraphWebApi/Startup.cs | 57 ++++--- MSGraphWebApi.sln | 8 +- .../OpenAPIDocumentCreatorMock.cs | 18 ++- OpenAPIService.Test/OpenAPIServiceShould.cs | 66 +++++---- OpenAPIService/Interfaces/IOpenApiService.cs | 30 ++++ OpenAPIService/OpenApiService.cs | 140 ++++++++++++++++-- .../CustomPIIFilterShould.cs | 37 ++--- TelemetryService.Test/IServiceProviderMock.cs | 41 +++++ ... => TelemetrySanitizerService.Test.csproj} | 3 +- TelemetryService.Test/TestProcessorNext.cs | 2 +- TelemetryService/CustomPIIFilter.cs | 35 +++-- ...sproj => TelemetrySanitizerService.csproj} | 1 + ...nsShould.cs => UtilityExtensionsShould.cs} | 2 +- UtilityService/UtilityConstants.cs | 23 +++ .../{Extensions.cs => UtilityExtensions.cs} | 4 +- .../{Utils.cs => UtilityFunctions.cs} | 5 +- UtilityService/UtilityService.csproj | 4 + 35 files changed, 885 insertions(+), 233 deletions(-) create mode 100644 ChangesService/Interfaces/IChangesService.cs create mode 100644 OpenAPIService/Interfaces/IOpenApiService.cs create mode 100644 TelemetryService.Test/IServiceProviderMock.cs rename TelemetryService.Test/{TelemetryService.Test.csproj => TelemetrySanitizerService.Test.csproj} (89%) rename TelemetryService/{TelemetryService.csproj => TelemetrySanitizerService.csproj} (83%) rename UtilityService.Test/{UtilityServiceExtensionsShould.cs => UtilityExtensionsShould.cs} (99%) create mode 100644 UtilityService/UtilityConstants.cs rename UtilityService/{Extensions.cs => UtilityExtensions.cs} (98%) rename UtilityService/{Utils.cs => UtilityFunctions.cs} (92%) diff --git a/ChangesService.Test/ChangeLogRecordsModelShould.cs b/ChangesService.Test/ChangeLogRecordsModelShould.cs index 70b8945da..9308d6e83 100644 --- a/ChangesService.Test/ChangeLogRecordsModelShould.cs +++ b/ChangesService.Test/ChangeLogRecordsModelShould.cs @@ -3,6 +3,7 @@ // ------------------------------------------------------------------------------------------------------------------------------------------------------ using System.Linq; +using ChangesService.Interfaces; using ChangesService.Models; using Xunit; @@ -10,6 +11,13 @@ namespace ChangesService.Test { public class ChangeLogRecordsModelShould { + private readonly IChangesService _changesService; + + public ChangeLogRecordsModelShould() + { + _changesService = new Services.ChangesService(); + } + [Fact] public void UpdateTotalItemsOnChangeLogsPropertySetter() { @@ -92,7 +100,7 @@ public void UpdateTotalPagesOnPageLimitPropertySetter() /// Optional. CreatedDateTime value for Reports /// workload. /// Sample - public static ChangeLogRecords GetChangeLogRecords(string variableDate = "2020-12-31T00:00:00.000Z") + public ChangeLogRecords GetChangeLogRecords(string variableDate = "2020-12-31T00:00:00.000Z") { // variableDate param will be used for specifying custom CreatedDateTime // value for Reports workload @@ -158,7 +166,7 @@ public static ChangeLogRecords GetChangeLogRecords(string variableDate = "2020-1 changeLogRecords = changeLogRecords.Replace("variableDate", variableDate); - return Services.ChangesService.DeserializeChangeLogRecords(changeLogRecords); + return _changesService.DeserializeChangeLogRecords(changeLogRecords); } } } diff --git a/ChangesService.Test/ChangesServiceShould.cs b/ChangesService.Test/ChangesServiceShould.cs index 61b64bd31..0b161bf16 100644 --- a/ChangesService.Test/ChangesServiceShould.cs +++ b/ChangesService.Test/ChangesServiceShould.cs @@ -2,6 +2,7 @@ // Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information. // ------------------------------------------------------------------------------------------------------------------------------------------------------ +using ChangesService.Interfaces; using ChangesService.Models; using System; using Xunit; @@ -11,6 +12,14 @@ namespace ChangesService.Test public class ChangesServiceShould { private readonly MicrosoftGraphProxyConfigs _graphProxyConfigs = new MicrosoftGraphProxyConfigs(); + private readonly IChangesService _changesService; + private readonly ChangeLogRecordsModelShould _changeLogRecordsModelShould; + + public ChangesServiceShould() + { + _changesService = new Services.ChangesService(); + _changeLogRecordsModelShould = new ChangeLogRecordsModelShould(); + } [Fact] public void ThrowArgumentNullExceptionOnDeserializeChangeLogRecordsIfJsonStringArgumentIsNull() @@ -20,7 +29,7 @@ public void ThrowArgumentNullExceptionOnDeserializeChangeLogRecordsIfJsonStringA // Act and Assert Assert.Throws(() => - Services.ChangesService.DeserializeChangeLogRecords(nullArgument)); + _changesService.DeserializeChangeLogRecords(nullArgument)); } [Fact] @@ -28,7 +37,7 @@ public void ThrowArgumentNullExceptionOnFilterChangeLogRecordsIfChangeLogRecords { // Act and Assert Assert.Throws(() => - Services.ChangesService.FilterChangeLogRecords(null, new ChangeLogSearchOptions(), new MicrosoftGraphProxyConfigs())); + _changesService.FilterChangeLogRecords(null, new ChangeLogSearchOptions(), new MicrosoftGraphProxyConfigs())); } [Fact] @@ -36,7 +45,7 @@ public void ThrowArgumentNullExceptionOnFilterChangeLogRecordsIfSearchOptionsArg { // Act and Assert Assert.Throws(() => - Services.ChangesService.FilterChangeLogRecords(new ChangeLogRecords(), null, new MicrosoftGraphProxyConfigs())); + _changesService.FilterChangeLogRecords(new ChangeLogRecords(), null, new MicrosoftGraphProxyConfigs())); } [Fact] @@ -44,7 +53,7 @@ public void ThrowArgumentNullExceptionOnFilterChangeLogRecordsIfGraphProxyConfig { // Act and Assert Assert.Throws(() => - Services.ChangesService.FilterChangeLogRecords(new ChangeLogRecords(), new ChangeLogSearchOptions(), null)); + _changesService.FilterChangeLogRecords(new ChangeLogRecords(), new ChangeLogSearchOptions(), null)); } [Fact] @@ -53,13 +62,13 @@ public void FilterChangeLogRecordsByWorkload() // Arrange var changeLogRecords = new ChangeLogRecords { - ChangeLogs = ChangeLogRecordsModelShould.GetChangeLogRecords().ChangeLogs + ChangeLogs = _changeLogRecordsModelShould.GetChangeLogRecords().ChangeLogs }; var searchOptions = new ChangeLogSearchOptions(workload: "Compliance"); // Act - var changeLog = Services.ChangesService.FilterChangeLogRecords(changeLogRecords, searchOptions, _graphProxyConfigs); + var changeLog = _changesService.FilterChangeLogRecords(changeLogRecords, searchOptions, _graphProxyConfigs); // Assert Assert.NotNull(changeLog); @@ -87,14 +96,14 @@ public void FilterChangeLogRecordsByDates() // Arrange var changeLogRecords = new ChangeLogRecords { - ChangeLogs = ChangeLogRecordsModelShould.GetChangeLogRecords().ChangeLogs + ChangeLogs = _changeLogRecordsModelShould.GetChangeLogRecords().ChangeLogs }; var startDate = DateTime.Parse("2020-01-01"); var endDate = DateTime.Parse("2020-10-01"); var searchOptions = new ChangeLogSearchOptions(startDate: startDate, endDate: endDate); // Act - var changeLog = Services.ChangesService.FilterChangeLogRecords(changeLogRecords, searchOptions, _graphProxyConfigs); + var changeLog = _changesService.FilterChangeLogRecords(changeLogRecords, searchOptions, _graphProxyConfigs); // Assert Assert.NotNull(changeLog); @@ -139,13 +148,13 @@ public void FilterChangeLogRecordsByDaysRange() DateTime varDate = DateTime.Today.AddDays(-30); var changeLogRecords = new ChangeLogRecords { - ChangeLogs = ChangeLogRecordsModelShould.GetChangeLogRecords(varDate.ToString("yyyy-MM-ddTHH:mm:ss")).ChangeLogs + ChangeLogs = _changeLogRecordsModelShould.GetChangeLogRecords(varDate.ToString("yyyy-MM-ddTHH:mm:ss")).ChangeLogs }; var searchOptions = new ChangeLogSearchOptions(daysRange: 60); // Act - var changeLog = Services.ChangesService.FilterChangeLogRecords(changeLogRecords, searchOptions, _graphProxyConfigs); + var changeLog = _changesService.FilterChangeLogRecords(changeLogRecords, searchOptions, _graphProxyConfigs); // Assert Assert.NotNull(changeLog); @@ -174,14 +183,14 @@ public void FilterChangeLogRecordsByStartDateAndDaysRange() // Arrange var changeLogRecords = new ChangeLogRecords { - ChangeLogs = ChangeLogRecordsModelShould.GetChangeLogRecords().ChangeLogs + ChangeLogs = _changeLogRecordsModelShould.GetChangeLogRecords().ChangeLogs }; var startDate = DateTime.Parse("2020-06-01"); var searchOptions = new ChangeLogSearchOptions(startDate: startDate, daysRange: 120); // Act - var changeLog = Services.ChangesService.FilterChangeLogRecords(changeLogRecords, searchOptions, _graphProxyConfigs); + var changeLog = _changesService.FilterChangeLogRecords(changeLogRecords, searchOptions, _graphProxyConfigs); // Assert Assert.NotNull(changeLog); @@ -224,14 +233,14 @@ public void FilterChangeLogRecordsByEndDateAndDaysRange() // Arrange var changeLogRecords = new ChangeLogRecords { - ChangeLogs = ChangeLogRecordsModelShould.GetChangeLogRecords().ChangeLogs + ChangeLogs = _changeLogRecordsModelShould.GetChangeLogRecords().ChangeLogs }; var endDate = DateTime.Parse("2021-01-01"); var searchOptions = new ChangeLogSearchOptions(endDate: endDate, daysRange: 30); // Act - var changeLog = Services.ChangesService.FilterChangeLogRecords(changeLogRecords, searchOptions, _graphProxyConfigs); + var changeLog = _changesService.FilterChangeLogRecords(changeLogRecords, searchOptions, _graphProxyConfigs); // Assert Assert.NotNull(changeLog); @@ -259,7 +268,7 @@ public void PaginateChangeLogRecordsFirstPage() // Arrange var changeLogRecords = new ChangeLogRecords { - ChangeLogs = ChangeLogRecordsModelShould.GetChangeLogRecords().ChangeLogs + ChangeLogs = _changeLogRecordsModelShould.GetChangeLogRecords().ChangeLogs }; var searchOptions = new ChangeLogSearchOptions @@ -269,7 +278,7 @@ public void PaginateChangeLogRecordsFirstPage() }; // Act - var changeLog = Services.ChangesService.FilterChangeLogRecords(changeLogRecords, searchOptions, _graphProxyConfigs); + var changeLog = _changesService.FilterChangeLogRecords(changeLogRecords, searchOptions, _graphProxyConfigs); // Assert -- fetch first two items from the changelog sample Assert.NotNull(changeLog); @@ -312,7 +321,7 @@ public void PaginateChangeLogRecordsMiddlePage() // Arrange var changeLogRecords = new ChangeLogRecords { - ChangeLogs = ChangeLogRecordsModelShould.GetChangeLogRecords().ChangeLogs + ChangeLogs = _changeLogRecordsModelShould.GetChangeLogRecords().ChangeLogs }; var searchOptions = new ChangeLogSearchOptions @@ -322,7 +331,7 @@ public void PaginateChangeLogRecordsMiddlePage() }; // Act - var changeLog = Services.ChangesService.FilterChangeLogRecords(changeLogRecords, searchOptions, _graphProxyConfigs); + var changeLog = _changesService.FilterChangeLogRecords(changeLogRecords, searchOptions, _graphProxyConfigs); // Assert -- fetch middle item from the changelog sample Assert.NotNull(changeLog); @@ -350,7 +359,7 @@ public void PaginateChangeLogRecordsLastPage() // Arrange var changeLogRecords = new ChangeLogRecords { - ChangeLogs = ChangeLogRecordsModelShould.GetChangeLogRecords().ChangeLogs + ChangeLogs = _changeLogRecordsModelShould.GetChangeLogRecords().ChangeLogs }; var searchOptions = new ChangeLogSearchOptions @@ -360,7 +369,7 @@ public void PaginateChangeLogRecordsLastPage() }; // Act - var changeLog = Services.ChangesService.FilterChangeLogRecords(changeLogRecords, searchOptions, _graphProxyConfigs); + var changeLog = _changesService.FilterChangeLogRecords(changeLogRecords, searchOptions, _graphProxyConfigs); // Assert -- fetch last item from the changelog sample Assert.NotNull(changeLog); diff --git a/ChangesService.Test/ChangesStoreShould.cs b/ChangesService.Test/ChangesStoreShould.cs index cdf48c47f..70df54403 100644 --- a/ChangesService.Test/ChangesStoreShould.cs +++ b/ChangesService.Test/ChangesStoreShould.cs @@ -24,9 +24,11 @@ public class ChangesStoreShould private readonly IHttpClientUtility _httpClientUtility; private readonly IMemoryCache _changesCache; private IChangesStore _changesStore; + private readonly IChangesService _changesService; public ChangesStoreShould() { + _changesService = new Services.ChangesService(); _httpClientUtility = new FileUtilityMock(); _changesCache = Create.MockedMemoryCache(); _configuration = new ConfigurationBuilder() @@ -39,16 +41,17 @@ public void ThrowArgumentNullExceptionOnConstructorIfArgumentsAreNull() { /* Act and Assert */ - Assert.Throws(() => new ChangesStore(null, _changesCache, _httpClientUtility)); // null configuration - Assert.Throws(() => new ChangesStore(_configuration, null, _httpClientUtility)); // null changesCache - Assert.Throws(() => new ChangesStore(_configuration, _changesCache, null)); // null httpClientUtility + Assert.Throws(() => new ChangesStore(null, _changesCache, _changesService, _httpClientUtility)); // null configuration + Assert.Throws(() => new ChangesStore(_configuration, null, _changesService, _httpClientUtility)); // null changesCache + Assert.Throws(() => new ChangesStore(_configuration, _changesCache, null, _httpClientUtility)); // null changesService + Assert.Throws(() => new ChangesStore(_configuration, _changesCache, _changesService, null)); // null httpClientUtility } [Fact] public async Task CorrectlySeedLocaleCachesOfChangeLogRecordsWhenMultipleRequestsMultipleLocaleReceived() { // Arrange - _changesStore = new ChangesStore(_configuration, _changesCache, _httpClientUtility); + _changesStore = new ChangesStore(_configuration, _changesCache, _changesService, _httpClientUtility); /* Act */ @@ -80,7 +83,7 @@ public async Task CorrectlySeedLocaleCachesOfChangeLogRecordsWhenMultipleRequest public async Task CorrectlySeedLocaleCachesOfChangeLogRecordsWhenMultipleRequestsSingleLocaleReceived() { // Arrange - _changesStore = new ChangesStore(_configuration, _changesCache, _httpClientUtility); + _changesStore = new ChangesStore(_configuration, _changesCache, _changesService, _httpClientUtility); /* Act */ @@ -114,7 +117,7 @@ public async Task CorrectlySeedLocaleCachesOfChangeLogRecordsWhenMultipleRequest public async Task SetDefaultLocaleInFetchChangeLogRecords(string locale) { // Arrange - _changesStore = new ChangesStore(_configuration, _changesCache, _httpClientUtility); + _changesStore = new ChangesStore(_configuration, _changesCache, _changesService, _httpClientUtility); /* Act */ diff --git a/ChangesService/ChangesService.csproj b/ChangesService/ChangesService.csproj index 5f934648d..fce38778f 100644 --- a/ChangesService/ChangesService.csproj +++ b/ChangesService/ChangesService.csproj @@ -6,11 +6,13 @@ + + diff --git a/ChangesService/Interfaces/IChangesService.cs b/ChangesService/Interfaces/IChangesService.cs new file mode 100644 index 000000000..b15fee492 --- /dev/null +++ b/ChangesService/Interfaces/IChangesService.cs @@ -0,0 +1,19 @@ +// ------------------------------------------------------------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information. +// ------------------------------------------------------------------------------------------------------------------------------------------------------ + +using ChangesService.Models; +using FileService.Interfaces; + +namespace ChangesService.Interfaces +{ + public interface IChangesService + { + ChangeLogRecords DeserializeChangeLogRecords(string jsonString); + + ChangeLogRecords FilterChangeLogRecords(ChangeLogRecords changeLogRecords, + ChangeLogSearchOptions searchOptions, + MicrosoftGraphProxyConfigs graphProxyConfigs, + IHttpClientUtility httpClientUtility = null); + } +} diff --git a/ChangesService/Services/ChangesService.cs b/ChangesService/Services/ChangesService.cs index ce4e1ab98..988deef81 100644 --- a/ChangesService/Services/ChangesService.cs +++ b/ChangesService/Services/ChangesService.cs @@ -3,9 +3,12 @@ // ------------------------------------------------------------------------------------------------------------------------------------------------------ using ChangesService.Common; +using ChangesService.Interfaces; using ChangesService.Models; using FileService.Common; using FileService.Interfaces; +using Microsoft.ApplicationInsights; +using Microsoft.ApplicationInsights.DataContracts; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System; @@ -13,23 +16,32 @@ using System.Linq; using System.Net.Http; using System.Threading.Tasks; +using UtilityService; namespace ChangesService.Services { /// /// Utility functions for transforming and filtering and objects. /// - public static class ChangesService + public class ChangesService : IChangesService { // Field to hold key-value pairs of url and workload names private static readonly Dictionary _urlWorkloadDict = new(); + private static readonly Dictionary _changesTraceProperties = + new() { { UtilityConstants.TelemetryPropertyKey_Changes, nameof(ChangesService)} }; + private readonly TelemetryClient _telemetryClient; + + public ChangesService(TelemetryClient telemetryClient = null) + { + _telemetryClient = telemetryClient; + } /// /// Deserializes a from a json string. /// /// The json string to deserialize /// The deserialized . - public static ChangeLogRecords DeserializeChangeLogRecords(string jsonString) + public ChangeLogRecords DeserializeChangeLogRecords(string jsonString) { if (string.IsNullOrEmpty(jsonString)) { @@ -53,11 +65,17 @@ public static ChangeLogRecords DeserializeChangeLogRecords(string jsonString) /// Optional. An implementation instance of . /// containing the filtered and/or paginated /// entries. - public static ChangeLogRecords FilterChangeLogRecords(ChangeLogRecords changeLogRecords, - ChangeLogSearchOptions searchOptions, + public ChangeLogRecords FilterChangeLogRecords(ChangeLogRecords changeLogRecords, + ChangeLogSearchOptions searchOptions, MicrosoftGraphProxyConfigs graphProxyConfigs, IHttpClientUtility httpClientUtility = null) { + _telemetryClient?.TrackTrace("Filtering changelog records", + SeverityLevel.Information, + _changesTraceProperties); + + string filterType = null; + if (changeLogRecords == null) { throw new ArgumentNullException(nameof(changeLogRecords), ChangesServiceConstants.ValueNullError); @@ -78,6 +96,8 @@ public static ChangeLogRecords FilterChangeLogRecords(ChangeLogRecords changeLog if (!string.IsNullOrEmpty(searchOptions.RequestUrl)) // filter by RequestUrl { + filterType = $"'Request Url: {searchOptions.RequestUrl}'"; + // Retrieve the workload name from the requestUrl var workload = RetrieveWorkloadNameFromRequestUrl(searchOptions, graphProxyConfigs, httpClientUtility) .GetAwaiter().GetResult(); @@ -88,6 +108,8 @@ public static ChangeLogRecords FilterChangeLogRecords(ChangeLogRecords changeLog } else if (!string.IsNullOrEmpty(searchOptions.Workload)) // filter by Workload { + filterType = $"'Workload: {searchOptions.Workload}'"; + // Search by the provided workload name enumerableChangeLog = FilterChangeLogRecordsByWorkload(changeLogRecords, searchOptions.Workload); @@ -128,6 +150,10 @@ public static ChangeLogRecords FilterChangeLogRecords(ChangeLogRecords changeLog ChangeLogs = enumerableChangeLog.ToList() }; + _telemetryClient?.TrackTrace($"Completed filtering changelog records by '{filterType}'", + SeverityLevel.Information, + _changesTraceProperties); + return PaginateChangeLogRecords(filteredChangeLogRecords, searchOptions); } @@ -226,10 +252,14 @@ private static IEnumerable FilterChangeLogRecordsByDates(ChangeLogRec /// Configuration settings for connecting to the Microsoft Graph Proxy. /// An implementation instance of . /// The workload name for the target request url. - private static async Task RetrieveWorkloadNameFromRequestUrl(ChangeLogSearchOptions searchOptions, + private async Task RetrieveWorkloadNameFromRequestUrl(ChangeLogSearchOptions searchOptions, MicrosoftGraphProxyConfigs graphProxy, IHttpClientUtility httpClientUtility) { + _telemetryClient?.TrackTrace($"Retrieving workload name for url '{searchOptions.RequestUrl}'", + SeverityLevel.Information, + _changesTraceProperties); + // Pull out the workload name value if it was already cached if (_urlWorkloadDict.TryGetValue(searchOptions.RequestUrl, out string workloadValue)) { @@ -275,6 +305,11 @@ private static async Task RetrieveWorkloadNameFromRequestUrl(ChangeLogSe // Cache the retrieved workload name _urlWorkloadDict.Add(searchOptions.RequestUrl, workloadName); + _telemetryClient?.TrackTrace($"Finished retrieving workload name for url '{searchOptions.RequestUrl}'. " + + $"Retrieved workload name: {workloadName}", + SeverityLevel.Information, + _changesTraceProperties); + return workloadName; // NB: No test coverage for this currently; requires a service call to the Graph proxy url } diff --git a/ChangesService/Services/ChangesStore.cs b/ChangesService/Services/ChangesStore.cs index 63b6ee1d3..07da1c0b8 100644 --- a/ChangesService/Services/ChangesStore.cs +++ b/ChangesService/Services/ChangesStore.cs @@ -8,12 +8,16 @@ using FileService.Common; using FileService.Extensions; using FileService.Interfaces; +using Microsoft.ApplicationInsights; +using Microsoft.ApplicationInsights.DataContracts; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Configuration; using System; +using System.Collections.Generic; using System.Globalization; using System.Net.Http; using System.Threading.Tasks; +using UtilityService; namespace ChangesService.Services { @@ -26,11 +30,19 @@ public class ChangesStore : IChangesStore private readonly IHttpClientUtility _httpClientUtility; private readonly IMemoryCache _changeLogCache; private readonly IConfiguration _configuration; + private readonly Dictionary _changesTraceProperties = + new() { { UtilityConstants.TelemetryPropertyKey_Changes, nameof(ChangesStore) } }; private readonly string _changeLogRelativeUrl; private readonly int _defaultRefreshTimeInHours; + private readonly TelemetryClient _telemetryClient; + private readonly IChangesService _changesService; - public ChangesStore(IConfiguration configuration, IMemoryCache changeLogCache, IHttpClientUtility httpClientUtility) + public ChangesStore(IConfiguration configuration, IMemoryCache changeLogCache, IChangesService changesService, + IHttpClientUtility httpClientUtility, TelemetryClient telemetryClient = null) { + _telemetryClient = telemetryClient; + _changesService = changesService ?? throw new ArgumentNullException(nameof(changesService), + $"{ ChangesServiceConstants.ValueNullError }: { nameof(changesService) }"); ; _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration), $"{ ChangesServiceConstants.ValueNullError }: { nameof(configuration) }"); _changeLogCache = changeLogCache ?? throw new ArgumentNullException(nameof(changeLogCache), @@ -51,9 +63,19 @@ public async Task FetchChangeLogRecordsAsync(CultureInfo cultu { var locale = cultureInfo.GetSupportedLocaleVariant().ToLower(); // lowercased in line with source files + _telemetryClient?.TrackTrace($"Retrieving changelog records for locale '{locale}' from in-memory cache '{locale}'", + SeverityLevel.Information, + _changesTraceProperties); + // Fetch cached changelog records ChangeLogRecords changeLogRecords = await _changeLogCache.GetOrCreateAsync(locale, cacheEntry => { + _telemetryClient?.TrackTrace($"In-memory cache '{locale}' empty. " + + $"Seeding changelog records for locale '{locale}' from Azure blob resource", + SeverityLevel.Information, + _changesTraceProperties); + + // Localized copy of changes is to be seeded by only one executing thread. lock (_changesLock) { /* Check whether a previous thread already seeded an @@ -61,9 +83,16 @@ public async Task FetchChangeLogRecordsAsync(CultureInfo cultu */ var lockedLocale = locale; var seededChangeLogRecords = _changeLogCache.Get(lockedLocale); + var sourceMsg = $"Return locale '{locale}' changelog records from in-memory cache"; if (seededChangeLogRecords != null) { + _telemetryClient?.TrackTrace($"In-memory cache '{lockedLocale}' of changelog records " + + $"already seeded by a concurrently running thread", + SeverityLevel.Information, + _changesTraceProperties); + sourceMsg = $"Return changelog records for locale '{lockedLocale}' from in-memory cache '{lockedLocale}'"; + // Already seeded by another thread return Task.FromResult(seededChangeLogRecords); } @@ -85,8 +114,14 @@ public async Task FetchChangeLogRecordsAsync(CultureInfo cultu var jsonFileContents = _httpClientUtility.ReadFromDocumentAsync(httpRequestMessage) .GetAwaiter().GetResult(); + sourceMsg = $"Successfully seeded changelog records for locale '{lockedLocale}' from source"; + + _telemetryClient?.TrackTrace(sourceMsg, + SeverityLevel.Information, + _changesTraceProperties); + // Return the changelog records from the file contents - return Task.FromResult(ChangesService.DeserializeChangeLogRecords(jsonFileContents)); + return Task.FromResult(_changesService.DeserializeChangeLogRecords(jsonFileContents)); } }); diff --git a/CodeSnippetsReflection/CodeSnippetsReflection.csproj b/CodeSnippetsReflection/CodeSnippetsReflection.csproj index 843795254..ffd917150 100644 --- a/CodeSnippetsReflection/CodeSnippetsReflection.csproj +++ b/CodeSnippetsReflection/CodeSnippetsReflection.csproj @@ -5,8 +5,13 @@ + + + + + diff --git a/CodeSnippetsReflection/SnippetsGenerator.cs b/CodeSnippetsReflection/SnippetsGenerator.cs index d1b609cbb..cbe5cfae3 100644 --- a/CodeSnippetsReflection/SnippetsGenerator.cs +++ b/CodeSnippetsReflection/SnippetsGenerator.cs @@ -6,6 +6,9 @@ using Microsoft.OData.Edm.Csdl; using CodeSnippetsReflection.LanguageGenerators; using System.Collections.Generic; +using Microsoft.ApplicationInsights.DataContracts; +using Microsoft.ApplicationInsights; +using UtilityService; namespace CodeSnippetsReflection { @@ -14,6 +17,9 @@ namespace CodeSnippetsReflection /// public class SnippetsGenerator : ISnippetsGenerator { + private readonly TelemetryClient _telemetryClient; + private readonly Dictionary _snippetsTraceProperties = + new() { { UtilityConstants.TelemetryPropertyKey_Snippets, nameof(SnippetsGenerator) } }; public static HashSet SupportedLanguages = new HashSet { "c#", @@ -48,8 +54,9 @@ public class SnippetsGenerator : ISnippetsGenerator /// /// Determines whether we are running the snippet generation in command line /// Full file path to the metadata - public SnippetsGenerator(bool isCommandLine = false, string customMetadataPath = null) + public SnippetsGenerator(bool isCommandLine = false, string customMetadataPath = null, TelemetryClient telemetryClient = null) { + _telemetryClient = telemetryClient; IsCommandLine = isCommandLine; LoadGraphMetadata(customMetadataPath); JavascriptExpressions = new JavascriptExpressions(); @@ -91,7 +98,7 @@ private void LoadGraphMetadata(string customMetadataPath) } /// - /// Entry point to generate snippets from the payload + /// Entry point to generate snippets from the payload /// /// /// @@ -101,6 +108,10 @@ public string ProcessPayloadRequest(HttpRequestMessage httpRequestMessage, strin var (edmModel, serviceRootUri) = GetModelAndServiceUriTuple(httpRequestMessage.RequestUri); var snippetModel = new SnippetModel(httpRequestMessage, serviceRootUri.AbsoluteUri, edmModel); + _telemetryClient?.TrackTrace($"Generating code snippet for '{language}' from the request payload", + SeverityLevel.Information, + _snippetsTraceProperties); + switch (language.ToLower()) { case "c#": @@ -120,7 +131,6 @@ public string ProcessPayloadRequest(HttpRequestMessage httpRequestMessage, strin default: throw new Exception("Invalid Language selected"); - } } diff --git a/GraphExplorerPermissionsService/Services/PermissionsStore.cs b/GraphExplorerPermissionsService/Services/PermissionsStore.cs index 02eec62f4..a1bfe8730 100644 --- a/GraphExplorerPermissionsService/Services/PermissionsStore.cs +++ b/GraphExplorerPermissionsService/Services/PermissionsStore.cs @@ -6,6 +6,8 @@ using FileService.Interfaces; using GraphExplorerPermissionsService.Interfaces; using GraphExplorerPermissionsService.Models; +using Microsoft.ApplicationInsights; +using Microsoft.ApplicationInsights.DataContracts; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Configuration; using Newtonsoft.Json; @@ -29,13 +31,16 @@ public class PermissionsStore : IPermissionsStore private readonly IFileUtility _fileUtility; private readonly IHttpClientUtility _httpClientUtility; private readonly IConfiguration _configuration; + private readonly TelemetryClient _telemetryClient; + private readonly Dictionary _permissionsTraceProperties = + new() { { UtilityConstants.TelemetryPropertyKey_Permissions, nameof(PermissionsStore)} }; private readonly string _permissionsContainerName; private readonly List _permissionsBlobNames; private readonly string _scopesInformation; private readonly int _defaultRefreshTimeInHours; // life span of the in-memory cache private const string DefaultLocale = "en-US"; // default locale language - private readonly object _permissionsLock = new object(); - private readonly object _scopesLock = new object(); + private readonly object _permissionsLock = new(); + private readonly object _scopesLock = new(); private static bool _permissionsRefreshed = false; private const string Delegated = "Delegated"; private const string Application = "Application"; @@ -46,8 +51,9 @@ public class PermissionsStore : IPermissionsStore private const string NullValueError = "Value cannot be null"; public PermissionsStore(IConfiguration configuration, IHttpClientUtility httpClientUtility, - IFileUtility fileUtility, IMemoryCache permissionsCache) + IFileUtility fileUtility, IMemoryCache permissionsCache, TelemetryClient telemetryClient=null) { + _telemetryClient = telemetryClient; _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration), $"{ NullValueError }: { nameof(configuration) }"); _permissionsCache = permissionsCache @@ -80,6 +86,12 @@ private void SeedPermissionsTables() foreach (string permissionFilePath in _permissionsBlobNames) { + _permissionsTraceProperties.Add(UtilityConstants.TelemetryPropertyKey_SanitizeIgnore, nameof(PermissionsStore)); + _telemetryClient?.TrackTrace($"Seeding permissions table from file source '{permissionFilePath}'", + SeverityLevel.Information, + _permissionsTraceProperties); + _permissionsTraceProperties.Remove(UtilityConstants.TelemetryPropertyKey_SanitizeIgnore); + string relativePermissionPath = FileServiceHelper.GetLocalizedFilePathSource(_permissionsContainerName, permissionFilePath); string jsonString = _fileUtility.ReadFromFile(relativePermissionPath).GetAwaiter().GetResult(); @@ -89,8 +101,8 @@ private void SeedPermissionsTables() if (permissionsObject.Count < 1) { - throw new InvalidOperationException($"The permissions data sources cannot be empty." + - $"Check the source file or check whether the file path is properly set. File path: " + + throw new InvalidOperationException("The permissions data sources cannot be empty." + + "Check the source file or check whether the file path is properly set. File path: " + $"{relativePermissionPath}"); } @@ -118,6 +130,10 @@ private void SeedPermissionsTables() _permissionsRefreshed = true; } } + + _telemetryClient?.TrackTrace("Finished seeding permissions tables", + SeverityLevel.Information, + _permissionsTraceProperties); } /// @@ -127,8 +143,17 @@ private void SeedPermissionsTables() /// The localized instance of permissions descriptions. private async Task>> GetOrCreatePermissionsDescriptionsAsync(string locale = DefaultLocale) { + _telemetryClient?.TrackTrace($"Retrieving permissions for locale '{locale}' from in-memory cache 'ScopesInfoList_{locale}'", + SeverityLevel.Information, + _permissionsTraceProperties); + var scopesInformationDictionary = await _permissionsCache.GetOrCreateAsync($"ScopesInfoList_{locale}", cacheEntry => { + _telemetryClient?.TrackTrace($"In-memory cache 'ScopesInfoList_{locale}' empty. " + + $"Seeding permissions for locale '{locale}' from Azure blob resource", + SeverityLevel.Information, + _permissionsTraceProperties); + /* Localized copy of permissions descriptions is to be seeded by only one executing thread. */ @@ -139,6 +164,7 @@ is to be seeded by only one executing thread. * during the lock. */ var seededScopesInfoDictionary = _permissionsCache.Get>>($"ScopesInfoList_{locale}"); + var sourceMsg = $"Return locale '{locale}' permissions from in-memory cache 'ScopesInfoList_{locale}'"; if (seededScopesInfoDictionary == null) { @@ -146,14 +172,26 @@ is to be seeded by only one executing thread. // Get file contents from source string scopesInfoJson = _fileUtility.ReadFromFile(relativeScopesInfoPath).GetAwaiter().GetResult(); + _telemetryClient?.TrackTrace($"Successfully seeded permissions for locale '{locale}' from Azure blob resource", + SeverityLevel.Information, + _permissionsTraceProperties); seededScopesInfoDictionary = CreateScopesInformationTables(scopesInfoJson); - cacheEntry.AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(_defaultRefreshTimeInHours); + sourceMsg = $"Return locale '{locale}' permissions from Azure blob resource"; + } + else + { + _telemetryClient?.TrackTrace($"In-memory cache 'ScopesInfoList_{locale}' of permissions " + + $"already seeded by a concurrently running thread", + SeverityLevel.Information, + _permissionsTraceProperties); } - /* Fetch the localized cached permissions descriptions - already seeded by previous thread. */ + _telemetryClient?.TrackTrace(sourceMsg, + SeverityLevel.Information, + _permissionsTraceProperties); + return Task.FromResult(seededScopesInfoDictionary); } }); @@ -172,6 +210,10 @@ private async Task>> G string branchName, string locale = DefaultLocale) { + _telemetryClient?.TrackTrace($"Retrieving permissions for locale '{locale}' from GitHub repository", + SeverityLevel.Information, + _permissionsTraceProperties); + string host = _configuration["BlobStorage:GithubHost"]; string repo = _configuration["BlobStorage:RepoName"]; @@ -185,6 +227,10 @@ private async Task>> G var scopesInformationDictionary = CreateScopesInformationTables(scopesInfoJson); + _telemetryClient?.TrackTrace($"Return permissions for locale '{locale}' from GitHub repository", + SeverityLevel.Information, + _permissionsTraceProperties); + return scopesInformationDictionary; } @@ -211,9 +257,16 @@ private IDictionary> CreateScopesI { if (string.IsNullOrEmpty(scopesInfoJson)) { + _telemetryClient?.TrackTrace($"{nameof(scopesInfoJson)} empty or null when creating the scopes information tables", + SeverityLevel.Error, + _permissionsTraceProperties); return null; } + _telemetryClient?.TrackTrace("Creating the scopes information tables", + SeverityLevel.Information, + _permissionsTraceProperties); + ScopesInformationList scopesInformationList = JsonConvert.DeserializeObject(scopesInfoJson); var _delegatedScopesInfoTable = new Dictionary(); @@ -229,6 +282,14 @@ private IDictionary> CreateScopesI _applicationScopesInfoTable.Add(applicationScopeInfo.ScopeName, applicationScopeInfo); } + _permissionsTraceProperties.Add(UtilityConstants.TelemetryPropertyKey_SanitizeIgnore, nameof(PermissionsStore)); + _telemetryClient?.TrackTrace("Finished creating the scopes information tables. " + + $"Delegated scopes count: {_delegatedScopesInfoTable.Count}. " + + $"Application scopes count: {_applicationScopesInfoTable.Count}", + SeverityLevel.Information, + _permissionsTraceProperties); + _permissionsTraceProperties.Remove(UtilityConstants.TelemetryPropertyKey_SanitizeIgnore); + return new Dictionary> { { Delegated, _delegatedScopesInfoTable }, @@ -282,6 +343,10 @@ public async Task> GetScopesAsync(string scopeType = "Del if (string.IsNullOrEmpty(requestUrl)) // fetch all permissions { + _telemetryClient?.TrackTrace("Fetching all permissions", + SeverityLevel.Information, + _permissionsTraceProperties); + List scopesListInfo = new List(); if (scopeType.Contains(Delegated)) @@ -305,6 +370,10 @@ public async Task> GetScopesAsync(string scopeType = "Del } } + _telemetryClient?.TrackTrace("Return all permissions", + SeverityLevel.Information, + _permissionsTraceProperties); + return scopesListInfo; } else // fetch permissions for a given request url and method @@ -321,6 +390,10 @@ public async Task> GetScopesAsync(string scopeType = "Del if (resultMatch == null) { + _telemetryClient?.TrackTrace($"Url '{requestUrl}' not found", + SeverityLevel.Error, + _permissionsTraceProperties); + return null; } @@ -334,6 +407,10 @@ public async Task> GetScopesAsync(string scopeType = "Del if (scopes == null) { + _telemetryClient?.TrackTrace($"No '{scopeType}' permissions found for the url '{requestUrl}' and method '{method}'", + SeverityLevel.Error, + _permissionsTraceProperties); + return null; } @@ -370,7 +447,12 @@ public async Task> GetScopesAsync(string scopeType = "Del } } + _telemetryClient?.TrackTrace($"Return '{scopeType}' permissions for url '{requestUrl}' and method '{method}'", + SeverityLevel.Information, + _permissionsTraceProperties); + return scopesList; + } } @@ -397,6 +479,10 @@ once per refresh cycle. */ if (!_permissionsRefreshed) { + _telemetryClient?.TrackTrace("Refreshing the permissions table", + SeverityLevel.Information, + _permissionsTraceProperties); + SeedPermissionsTables(); _permissionsRefreshed = true; } diff --git a/GraphExplorerSamplesService/GraphExplorerSamplesService.csproj b/GraphExplorerSamplesService/GraphExplorerSamplesService.csproj index b60742a99..003dce8dd 100644 --- a/GraphExplorerSamplesService/GraphExplorerSamplesService.csproj +++ b/GraphExplorerSamplesService/GraphExplorerSamplesService.csproj @@ -5,6 +5,7 @@ + @@ -12,6 +13,7 @@ + diff --git a/GraphExplorerSamplesService/Services/SamplesStore.cs b/GraphExplorerSamplesService/Services/SamplesStore.cs index 3285faec4..80219b130 100644 --- a/GraphExplorerSamplesService/Services/SamplesStore.cs +++ b/GraphExplorerSamplesService/Services/SamplesStore.cs @@ -6,11 +6,15 @@ using FileService.Interfaces; using GraphExplorerSamplesService.Interfaces; using GraphExplorerSamplesService.Models; +using Microsoft.ApplicationInsights; +using Microsoft.ApplicationInsights.DataContracts; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Configuration; using System; +using System.Collections.Generic; using System.Net.Http; using System.Threading.Tasks; +using UtilityService; namespace GraphExplorerSamplesService.Services { @@ -24,14 +28,18 @@ public class SamplesStore : ISamplesStore private readonly IHttpClientUtility _httpClientUtility; private readonly IMemoryCache _samplesCache; private readonly IConfiguration _configuration; + private readonly Dictionary SamplesTraceProperties = + new() { { UtilityConstants.TelemetryPropertyKey_Samples, nameof(SamplesStore)} }; private readonly string _sampleQueriesContainerName; private readonly string _sampleQueriesBlobName; private readonly int _defaultRefreshTimeInHours; private const string NullValueError = "Value cannot be null"; + private readonly TelemetryClient _telemetryClient; public SamplesStore(IConfiguration configuration, IHttpClientUtility httpClientUtility, - IFileUtility fileUtility, IMemoryCache samplesCache) + IFileUtility fileUtility, IMemoryCache samplesCache, TelemetryClient telemetryClient = null) { + _telemetryClient = telemetryClient; _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration), $"{ NullValueError }: { nameof(configuration) }"); _httpClientUtility = httpClientUtility @@ -39,7 +47,7 @@ public SamplesStore(IConfiguration configuration, IHttpClientUtility httpClientU _fileUtility = fileUtility ?? throw new ArgumentNullException(nameof(fileUtility), $"{ NullValueError }: { nameof(fileUtility) }"); _samplesCache = samplesCache - ?? throw new ArgumentNullException(nameof(samplesCache), $"{ NullValueError }: { nameof(samplesCache) }"); ; + ?? throw new ArgumentNullException(nameof(samplesCache), $"{ NullValueError }: { nameof(samplesCache) }"); _sampleQueriesContainerName = _configuration["BlobStorage:Containers:SampleQueries"]; _sampleQueriesBlobName = _configuration["BlobStorage:Blobs:SampleQueries"]; _defaultRefreshTimeInHours = FileServiceHelper.GetFileCacheRefreshTime(configuration["FileCacheRefreshTimeInHours:SampleQueries"]); @@ -53,41 +61,67 @@ public SamplesStore(IConfiguration configuration, IHttpClientUtility httpClientU /// The deserialized instance of a . public async Task FetchSampleQueriesListAsync(string locale) { + _telemetryClient?.TrackTrace($"Retrieving sample queries list for locale '{locale}' from in-memory cache '{locale}'", + SeverityLevel.Information, + SamplesTraceProperties); + + string sourceMsg = $"Return sample queries list for locale '{locale}' from in-memory cache '{locale}'"; + // Fetch cached sample queries SampleQueriesList sampleQueriesList = await _samplesCache.GetOrCreateAsync(locale, cacheEntry => - { - // Localized copy of samples is to be seeded by only one executing thread. - lock (_samplesLock) - { - /* Check whether a previous thread already seeded an + { + _telemetryClient?.TrackTrace($"In-memory cache '{locale}' empty. " + + $"Seeding sample queries list from Azure Blob resource", + SeverityLevel.Information, + SamplesTraceProperties); + + // Localized copy of samples is to be seeded by only one executing thread. + lock (_samplesLock) + { + /* Check whether a previous thread already seeded an * instance of the localized samples during the lock. */ - var lockedLocale = locale; - var seededSampleQueriesList = _samplesCache?.Get(lockedLocale); + var lockedLocale = locale; + var seededSampleQueriesList = _samplesCache?.Get(lockedLocale); + + if (seededSampleQueriesList != null) + { + _telemetryClient?.TrackTrace($"In-memory cache '{lockedLocale}' of sample queries list " + + $"already seeded by a concurrently running thread", + SeverityLevel.Information, + SamplesTraceProperties); + sourceMsg = $"Return sample queries list for locale '{lockedLocale}' from in-memory cache '{lockedLocale}'"; + + return Task.FromResult(seededSampleQueriesList); + } - if (seededSampleQueriesList != null) - { - return Task.FromResult(seededSampleQueriesList); - } + cacheEntry.AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(_defaultRefreshTimeInHours); - cacheEntry.AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(_defaultRefreshTimeInHours); + // Fetch the requisite sample path source based on the locale + string queriesFilePathSource = + FileServiceHelper.GetLocalizedFilePathSource(_sampleQueriesContainerName, _sampleQueriesBlobName, lockedLocale); - // Fetch the requisite sample path source based on the locale - string queriesFilePathSource = - FileServiceHelper.GetLocalizedFilePathSource(_sampleQueriesContainerName, _sampleQueriesBlobName, lockedLocale); + // Get the file contents from source + string jsonFileContents = _fileUtility.ReadFromFile(queriesFilePathSource).GetAwaiter().GetResult(); - // Get the file contents from source - string jsonFileContents = _fileUtility.ReadFromFile(queriesFilePathSource).GetAwaiter().GetResult(); + _telemetryClient?.TrackTrace($"Successfully seeded sample queries list for locale '{lockedLocale}' from Azure Blob resource", + SeverityLevel.Information, + SamplesTraceProperties); - /* Current business process only supports ordering of the English + /* Current business process only supports ordering of the English translation of the sample queries. */ - bool orderSamples = lockedLocale.Equals("en-us", StringComparison.OrdinalIgnoreCase); + bool orderSamples = lockedLocale.Equals("en-us", StringComparison.OrdinalIgnoreCase); - // Return the list of the sample queries from the file contents - return Task.FromResult(SamplesService.DeserializeSampleQueriesList(jsonFileContents, orderSamples)); - } - }); + sourceMsg = $"Return sample queries list for locale '{lockedLocale}' from Azure Blob resource"; + + return Task.FromResult(DeserializeSamplesList(jsonFileContents, locale)); + } + }); + + _telemetryClient?.TrackTrace(sourceMsg, + SeverityLevel.Information, + SamplesTraceProperties); return sampleQueriesList; } @@ -102,6 +136,10 @@ translation of the sample queries. /// The deserialized instance of a . public async Task FetchSampleQueriesListAsync(string locale, string org, string branchName) { + _telemetryClient?.TrackTrace($"Retrieving sample queries list for locale '{locale}' from GitHub repository.", + SeverityLevel.Information, + SamplesTraceProperties); + string host = _configuration["BlobStorage:GithubHost"]; string repo = _configuration["BlobStorage:RepoName"]; @@ -116,6 +154,10 @@ public async Task FetchSampleQueriesListAsync(string locale, string jsonFileContents = await _httpClientUtility.ReadFromDocumentAsync(httpRequestMessage); + _telemetryClient?.TrackTrace($"Return sample queries list for locale '{locale}' from GitHub repository", + SeverityLevel.Information, + SamplesTraceProperties); + return DeserializeSamplesList(jsonFileContents, locale); } diff --git a/GraphWebApi/Controllers/ChangesController.cs b/GraphWebApi/Controllers/ChangesController.cs index cecbfad51..f959499ee 100644 --- a/GraphWebApi/Controllers/ChangesController.cs +++ b/GraphWebApi/Controllers/ChangesController.cs @@ -3,16 +3,20 @@ // ------------------------------------------------------------------------------------------------------------------------------------------------------ using System; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using ChangesService.Common; using ChangesService.Interfaces; using ChangesService.Models; using FileService.Interfaces; +using Microsoft.ApplicationInsights; +using Microsoft.ApplicationInsights.DataContracts; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Localization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; +using UtilityService; namespace GraphWebApi.Controllers { @@ -22,12 +26,19 @@ public class ChangesController : ControllerBase private readonly IChangesStore _changesStore; private readonly IConfiguration _configuration; private readonly IHttpClientUtility _httpClientUtility; + private readonly Dictionary _changesTraceProperties = + new() { { UtilityConstants.TelemetryPropertyKey_Changes, nameof(ChangesController) } }; + private readonly TelemetryClient _telemetryClient; + private readonly IChangesService _changesService; - public ChangesController(IChangesStore changesStore, IConfiguration configuration, IHttpClientUtility httpClientUtility) + public ChangesController(IChangesStore changesStore, IConfiguration configuration, IChangesService changesService, + IHttpClientUtility httpClientUtility, TelemetryClient telemetryClient) { + _telemetryClient = telemetryClient; _changesStore = changesStore; _configuration = configuration; _httpClientUtility = httpClientUtility; + _changesService = changesService; } // Gets the changelog records @@ -61,9 +72,13 @@ public async Task GetChangesAsync( var cultureFeature = HttpContext.Features.Get(); var cultureInfo = cultureFeature.RequestCulture.Culture; + _telemetryClient?.TrackTrace($"Request to fetch changelog records for the requested culture info '{cultureInfo}'", + SeverityLevel.Information, + _changesTraceProperties); // Fetch the changelog records var changeLog = await _changesStore.FetchChangeLogRecordsAsync(cultureInfo); + // Filter the changelog records if (changeLog.ChangeLogs.Any()) { @@ -76,8 +91,7 @@ public async Task GetChangesAsync( GraphVersion = graphVersion }; - changeLog = ChangesService.Services.ChangesService - .FilterChangeLogRecords(changeLog, searchOptions, graphProxyConfigs, _httpClientUtility); + changeLog = _changesService.FilterChangeLogRecords(changeLog, searchOptions, graphProxyConfigs, _httpClientUtility); } else { @@ -87,23 +101,35 @@ public async Task GetChangesAsync( if (!changeLog.ChangeLogs.Any()) { + _telemetryClient?.TrackTrace($"Search options not found in: requestUrl, workload, daysRange, startDate, endDate properties of changelog records", + SeverityLevel.Error, + _changesTraceProperties); // Filtered items yielded no result return NotFound(); } - + _changesTraceProperties.Add(UtilityConstants.TelemetryPropertyKey_SanitizeIgnore, nameof(ChangesController)); + _telemetryClient?.TrackTrace($"Fetched {changeLog.CurrentItems} changes", + SeverityLevel.Information, + _changesTraceProperties); return Ok(changeLog); } - catch (InvalidOperationException ex) + catch (InvalidOperationException invalidOpsException) { - return new JsonResult(ex.Message) { StatusCode = StatusCodes.Status400BadRequest }; + _telemetryClient?.TrackException(invalidOpsException, + _changesTraceProperties); + return new JsonResult(invalidOpsException.Message) { StatusCode = StatusCodes.Status500InternalServerError }; } - catch (ArgumentException ex) + catch (ArgumentException argException) { - return new JsonResult(ex.Message) { StatusCode = StatusCodes.Status404NotFound }; + _telemetryClient?.TrackException(argException, + _changesTraceProperties); + return new JsonResult(argException.Message) { StatusCode = StatusCodes.Status404NotFound }; } - catch (Exception ex) + catch (Exception exception) { - return new JsonResult(ex.Message) { StatusCode = StatusCodes.Status500InternalServerError }; + _telemetryClient?.TrackException(exception, + _changesTraceProperties); + return new JsonResult(exception.Message) { StatusCode = StatusCodes.Status500InternalServerError }; } } } diff --git a/GraphWebApi/Controllers/GraphExplorerPermissionsController.cs b/GraphWebApi/Controllers/GraphExplorerPermissionsController.cs index 8dd2215f8..2a4c11101 100644 --- a/GraphWebApi/Controllers/GraphExplorerPermissionsController.cs +++ b/GraphWebApi/Controllers/GraphExplorerPermissionsController.cs @@ -8,8 +8,11 @@ using GraphExplorerPermissionsService.Interfaces; using GraphExplorerPermissionsService.Models; using GraphWebApi.Common; +using Microsoft.ApplicationInsights; +using Microsoft.ApplicationInsights.DataContracts; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using UtilityService; namespace GraphWebApi.Controllers { @@ -19,9 +22,13 @@ namespace GraphWebApi.Controllers public class GraphExplorerPermissionsController : ControllerBase { private readonly IPermissionsStore _permissionsStore; + private readonly Dictionary _permissionsTraceProperties = + new() { { UtilityConstants.TelemetryPropertyKey_Permissions, nameof(GraphExplorerPermissionsController) } }; + private readonly TelemetryClient _telemetryClient; - public GraphExplorerPermissionsController(IPermissionsStore permissionsStore) + public GraphExplorerPermissionsController(IPermissionsStore permissionsStore, TelemetryClient telemetryClient) { + _telemetryClient = telemetryClient; _permissionsStore = permissionsStore; } @@ -37,6 +44,9 @@ public async Task GetPermissionScopes([FromQuery]string scopeType try { string localeCode = RequestHelper.GetPreferredLocaleLanguage(Request) ?? Constants.DefaultLocale; + _telemetryClient?.TrackTrace($"Request to fetch permissions for locale '{localeCode}'", + SeverityLevel.Information, + _permissionsTraceProperties); List result = null; @@ -59,16 +69,25 @@ public async Task GetPermissionScopes([FromQuery]string scopeType method: method); } + _permissionsTraceProperties.Add(UtilityConstants.TelemetryPropertyKey_SanitizeIgnore, nameof(GraphExplorerPermissionsController)); + _telemetryClient?.TrackTrace($"Fetched {result.Count} permissions", + SeverityLevel.Information, + _permissionsTraceProperties); + return result == null ? NotFound() : Ok(result); } catch (ArgumentNullException argNullException) { + _telemetryClient?.TrackException(argNullException, + _permissionsTraceProperties); return new JsonResult(argNullException.Message) { StatusCode = StatusCodes.Status400BadRequest }; } catch (Exception exception) { // Any 'InvalidOperationException' will also be caught here - these are classified as error 500 + _telemetryClient?.TrackException(exception, + _permissionsTraceProperties); return new JsonResult(exception.Message) { StatusCode = StatusCodes.Status500InternalServerError }; } } diff --git a/GraphWebApi/Controllers/GraphExplorerSamplesController.cs b/GraphWebApi/Controllers/GraphExplorerSamplesController.cs index 0ad60e4c0..13fd56566 100644 --- a/GraphWebApi/Controllers/GraphExplorerSamplesController.cs +++ b/GraphWebApi/Controllers/GraphExplorerSamplesController.cs @@ -14,6 +14,9 @@ using System.Linq; using GraphWebApi.Common; using GraphExplorerSamplesService.Interfaces; +using Microsoft.ApplicationInsights.DataContracts; +using UtilityService; +using Microsoft.ApplicationInsights; namespace GraphWebApi.Controllers { @@ -21,9 +24,13 @@ namespace GraphWebApi.Controllers public class GraphExplorerSamplesController : ControllerBase { private readonly ISamplesStore _samplesStore; + private readonly Dictionary _samplesTraceProperties = + new() { { UtilityConstants.TelemetryPropertyKey_Samples, nameof(GraphExplorerSamplesController)} }; + private readonly TelemetryClient _telemetryClient; - public GraphExplorerSamplesController(ISamplesStore samplesStore) + public GraphExplorerSamplesController(ISamplesStore samplesStore, TelemetryClient telemetryClient) { + _telemetryClient = telemetryClient; _samplesStore = samplesStore; } @@ -58,15 +65,23 @@ public async Task GetSampleQueriesListAsync(string search, string if (filteredSampleQueries.Count == 0) { - // Search parameter not found in list of sample queries + _telemetryClient?.TrackTrace($"Search value: '{search}' not found in: category, humanName or tip properties of sample queries", + SeverityLevel.Error, + _samplesTraceProperties); return NotFound(); } - // Success; return the found list of sample queries from filtered search + _samplesTraceProperties.Add(UtilityConstants.TelemetryPropertyKey_SanitizeIgnore, nameof(GraphExplorerSamplesController)); + _telemetryClient?.TrackTrace($"{filteredSampleQueries.Count} sample queries found from search value '{search}'", + SeverityLevel.Information, + _samplesTraceProperties); + return Ok(filteredSampleQueries); } catch (Exception exception) { + _telemetryClient?.TrackException(exception, + _samplesTraceProperties); return new JsonResult(exception.Message) { StatusCode = StatusCodes.Status500InternalServerError }; } } @@ -92,7 +107,10 @@ public async Task GetSampleQueryByIdAsync(string id, string org, if (sampleQueryById == null) { - return NotFound(); // sample query with the given id doesn't exist in the list of sample queries + _telemetryClient?.TrackTrace($"Sample query with id: {id} doesn't exist in the list of sample queries", + SeverityLevel.Error, + _samplesTraceProperties); + return NotFound(); } // Success; return the found sample query @@ -100,6 +118,8 @@ public async Task GetSampleQueryByIdAsync(string id, string org, } catch (Exception exception) { + _telemetryClient?.TrackException(exception, + _samplesTraceProperties); return new JsonResult(exception.Message) { StatusCode = StatusCodes.Status500InternalServerError }; } } @@ -162,11 +182,15 @@ public async Task UpdateSampleQueryAsync(string id, [FromBody]Sam } catch (InvalidOperationException invalidOpsException) { + _telemetryClient?.TrackException(invalidOpsException, + _samplesTraceProperties); // sample query with provided id not found return new JsonResult(invalidOpsException.Message) { StatusCode = StatusCodes.Status404NotFound }; } catch (Exception exception) { + _telemetryClient?.TrackException(exception, + _samplesTraceProperties); return new JsonResult(exception.Message) { StatusCode = StatusCodes.Status500InternalServerError }; } } @@ -225,6 +249,8 @@ public async Task CreateSampleQueryAsync([FromBody]SampleQueryMod } catch (Exception exception) { + _telemetryClient?.TrackException(exception, + _samplesTraceProperties); return new JsonResult(exception.Message) { StatusCode = StatusCodes.Status500InternalServerError }; } } @@ -290,11 +316,15 @@ public async Task DeleteSampleQueryAsync(string id) } catch (InvalidOperationException invalidOpsException) { + _telemetryClient?.TrackException(invalidOpsException, + _samplesTraceProperties); // Sample query with provided id not found return new JsonResult(invalidOpsException.Message) { StatusCode = StatusCodes.Status404NotFound }; } catch (Exception exception) { + _telemetryClient?.TrackException(exception, + _samplesTraceProperties); return new JsonResult(exception.Message) { StatusCode = StatusCodes.Status500InternalServerError }; } } @@ -308,19 +338,25 @@ public async Task DeleteSampleQueryAsync(string id) private async Task FetchSampleQueriesListAsync(string org, string branchName) { string locale = RequestHelper.GetPreferredLocaleLanguage(Request); + _telemetryClient?.TrackTrace($"Request to fetch samples for locale '{locale}'", + SeverityLevel.Information, + _samplesTraceProperties); SampleQueriesList sampleQueriesList; if (!string.IsNullOrEmpty(org) && !string.IsNullOrEmpty(branchName)) { - // Fetch samples file from Github sampleQueriesList = await _samplesStore.FetchSampleQueriesListAsync(locale, org, branchName); } else { - // Fetch sample queries from Azure Blob sampleQueriesList = await _samplesStore.FetchSampleQueriesListAsync(locale); } + _samplesTraceProperties.Add(UtilityConstants.TelemetryPropertyKey_SanitizeIgnore, nameof(GraphExplorerSamplesController)); + _telemetryClient?.TrackTrace($"Fetched {sampleQueriesList?.SampleQueries.Count} samples", + SeverityLevel.Information, + _samplesTraceProperties); + return sampleQueriesList; } diff --git a/GraphWebApi/Controllers/GraphExplorerSnippetsController.cs b/GraphWebApi/Controllers/GraphExplorerSnippetsController.cs index 1274c3a03..bbf03cfcc 100644 --- a/GraphWebApi/Controllers/GraphExplorerSnippetsController.cs +++ b/GraphWebApi/Controllers/GraphExplorerSnippetsController.cs @@ -6,6 +6,10 @@ using CodeSnippetsReflection; using GraphWebApi.Models; using Microsoft.AspNetCore.Http; +using System.Collections.Generic; +using Microsoft.ApplicationInsights.DataContracts; +using Microsoft.ApplicationInsights; +using UtilityService; namespace GraphWebApi.Controllers { @@ -15,24 +19,35 @@ namespace GraphWebApi.Controllers public class GraphExplorerSnippetsController : ControllerBase { private readonly ISnippetsGenerator _snippetGenerator; + private readonly Dictionary _snippetsTraceProperties = + new() { { UtilityConstants.TelemetryPropertyKey_Snippets, nameof(GraphExplorerSnippetsController) } }; + private readonly TelemetryClient _telemetryClient; - public GraphExplorerSnippetsController(ISnippetsGenerator snippetGenerator) + public GraphExplorerSnippetsController(ISnippetsGenerator snippetGenerator, TelemetryClient telemetryClient) { + _telemetryClient = telemetryClient; _snippetGenerator = snippetGenerator; } - //Default Service Page GET + //Default Service Page GET [HttpGet] [Produces("application/json")] public IActionResult Get(string arg) { if(string.IsNullOrWhiteSpace(arg)) { + _telemetryClient?.TrackTrace("Fetching code snippet", + SeverityLevel.Information, + _snippetsTraceProperties); + string result = "Graph Explorer Snippets Generator"; return new OkObjectResult(new CodeSnippetResult { Code = "null", StatusCode = false, Message = result, Language = "Default C#" }); } else { + _telemetryClient?.TrackTrace($"Fetching snippet based on '{arg}'", + SeverityLevel.Information, + _snippetsTraceProperties); string result = "Graph Explorer Snippets Generator"; return new OkObjectResult(new CodeSnippetResult { Code = "null", StatusCode = false, Message = result, Language = "Default C#" }); } @@ -50,11 +65,23 @@ public async Task PostAsync(string lang = "c#") try { using HttpRequestMessage requestPayload = await streamContent.ReadAsHttpRequestMessageAsync().ConfigureAwait(false); + + _telemetryClient?.TrackTrace($"Processing the request payload: '{requestPayload}'", + SeverityLevel.Information, + _snippetsTraceProperties); + var response = _snippetGenerator.ProcessPayloadRequest(requestPayload, lang); + + _telemetryClient?.TrackTrace("Finished generating a code snippet", + SeverityLevel.Information, + _snippetsTraceProperties); + return new StringResult(response); } catch (Exception e) { + _telemetryClient?.TrackException(e, + _snippetsTraceProperties); return new BadRequestObjectResult(e.Message); } } diff --git a/GraphWebApi/Controllers/OpenApiController.cs b/GraphWebApi/Controllers/OpenApiController.cs index 8b4c658e7..93adc055b 100644 --- a/GraphWebApi/Controllers/OpenApiController.cs +++ b/GraphWebApi/Controllers/OpenApiController.cs @@ -3,6 +3,7 @@ // ------------------------------------------------------------------------------------------------------------------------------------------------------ using GraphWebApi.Models; +using Microsoft.ApplicationInsights; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; @@ -10,10 +11,13 @@ using Microsoft.OpenApi.Services; using OpenAPIService; using OpenAPIService.Common; +using OpenAPIService.Interfaces; using System; +using System.Collections.Generic; using System.Globalization; using System.IO; using System.Threading.Tasks; +using UtilityService; namespace GraphWebApi.Controllers { @@ -24,10 +28,16 @@ namespace GraphWebApi.Controllers public class OpenApiController : ControllerBase { private readonly IConfiguration _configuration; + private static readonly Dictionary _openApiTraceProperties = + new() { { UtilityConstants.TelemetryPropertyKey_OpenApi, nameof(OpenApiController)} }; + private readonly TelemetryClient _telemetryClient; + private readonly IOpenApiService _openApiService; - public OpenApiController(IConfiguration configuration) + public OpenApiController(IConfiguration configuration, IOpenApiService openApiService, TelemetryClient telemetryClient) { + _telemetryClient = telemetryClient; _configuration = configuration; + _openApiService = openApiService; } [Route("openapi")] @@ -55,20 +65,20 @@ public async Task Get( throw new InvalidOperationException($"Unsupported {nameof(graphVersion)} provided: '{graphVersion}'"); } - OpenApiDocument source = await OpenApiService.GetGraphOpenApiDocumentAsync(graphUri, forceRefresh); + OpenApiDocument source = await _openApiService.GetGraphOpenApiDocumentAsync(graphUri, forceRefresh); - var predicate = OpenApiService.CreatePredicate(operationIds: operationIds, - tags: tags, - url: url, - source: source, - graphVersion: styleOptions.GraphVersion, - forceRefresh: forceRefresh); + var predicate = _openApiService.CreatePredicate(operationIds: operationIds, + tags: tags, + url: url, + source: source, + graphVersion: styleOptions.GraphVersion, + forceRefresh: forceRefresh); - var subsetOpenApiDocument = OpenApiService.CreateFilteredDocument(source, title, styleOptions.GraphVersion, predicate); + var subsetOpenApiDocument = _openApiService.CreateFilteredDocument(source, title, styleOptions.GraphVersion, predicate); - subsetOpenApiDocument = OpenApiService.ApplyStyle(styleOptions.Style, subsetOpenApiDocument); + subsetOpenApiDocument = _openApiService.ApplyStyle(styleOptions.Style, subsetOpenApiDocument); - var stream = OpenApiService.SerializeOpenApiDocument(subsetOpenApiDocument, styleOptions); + var stream = _openApiService.SerializeOpenApiDocument(subsetOpenApiDocument, styleOptions); if (styleOptions.OpenApiFormat == "yaml") { @@ -79,16 +89,25 @@ public async Task Get( return new FileStreamResult(stream, "application/json"); } } - catch (InvalidOperationException ex) + catch (InvalidOperationException invalidOps) { - return new JsonResult(ex.Message) { StatusCode = StatusCodes.Status400BadRequest }; + _telemetryClient?.TrackException(invalidOps, + _openApiTraceProperties); + + return new JsonResult(invalidOps.Message) { StatusCode = StatusCodes.Status400BadRequest }; } - catch (ArgumentException ex) + catch (ArgumentException argException) { - return new JsonResult(ex.Message) { StatusCode = StatusCodes.Status404NotFound }; + _telemetryClient?.TrackException(argException, + _openApiTraceProperties); + + return new JsonResult(argException.Message) { StatusCode = StatusCodes.Status404NotFound }; } catch (Exception ex) { + _telemetryClient?.TrackException(ex, + _openApiTraceProperties); + return new JsonResult(ex.Message) { StatusCode = StatusCodes.Status500InternalServerError }; } } @@ -110,20 +129,20 @@ public async Task Post( { OpenApiStyleOptions styleOptions = new OpenApiStyleOptions(style, openApiVersion, graphVersion, format); - OpenApiDocument source = await OpenApiService.ConvertCsdlToOpenApiAsync(Request.Body); + OpenApiDocument source = await _openApiService.ConvertCsdlToOpenApiAsync(Request.Body); - var predicate = OpenApiService.CreatePredicate(operationIds: operationIds, + var predicate = _openApiService.CreatePredicate(operationIds: operationIds, tags: tags, url: url, source: source, graphVersion: styleOptions.GraphVersion, forceRefresh: forceRefresh); - var subsetOpenApiDocument = OpenApiService.CreateFilteredDocument(source, title, styleOptions.GraphVersion, predicate); + var subsetOpenApiDocument = _openApiService.CreateFilteredDocument(source, title, styleOptions.GraphVersion, predicate); - subsetOpenApiDocument = OpenApiService.ApplyStyle(styleOptions.Style, subsetOpenApiDocument); + subsetOpenApiDocument = _openApiService.ApplyStyle(styleOptions.Style, subsetOpenApiDocument); - var stream = OpenApiService.SerializeOpenApiDocument(subsetOpenApiDocument, styleOptions); + var stream = _openApiService.SerializeOpenApiDocument(subsetOpenApiDocument, styleOptions); if (styleOptions.OpenApiFormat == "yaml") { @@ -134,16 +153,25 @@ public async Task Post( return new FileStreamResult(stream, "application/json"); } } - catch (InvalidOperationException ex) + catch (InvalidOperationException invalidOps) { - return new JsonResult(ex.Message) { StatusCode = StatusCodes.Status400BadRequest }; + _telemetryClient?.TrackException(invalidOps, + _openApiTraceProperties); + + return new JsonResult(invalidOps.Message) { StatusCode = StatusCodes.Status400BadRequest }; } - catch (ArgumentException ex) + catch (ArgumentException argException) { - return new JsonResult(ex.Message) { StatusCode = StatusCodes.Status404NotFound }; + _telemetryClient?.TrackException(argException, + _openApiTraceProperties); + + return new JsonResult(argException.Message) { StatusCode = StatusCodes.Status404NotFound }; } catch (Exception ex) { + _telemetryClient?.TrackException(ex, + _openApiTraceProperties); + return new JsonResult(ex.Message) { StatusCode = StatusCodes.Status500InternalServerError }; } } @@ -167,22 +195,31 @@ public async Task Get([FromQuery]string graphVersion = null, throw new InvalidOperationException($"Unsupported {nameof(graphVersion)} provided: '{graphVersion}'"); } - var graphOpenApi = await OpenApiService.GetGraphOpenApiDocumentAsync(graphUri, forceRefresh); + var graphOpenApi = await _openApiService.GetGraphOpenApiDocumentAsync(graphUri, forceRefresh); await WriteIndex(Request.Scheme + "://" + Request.Host.Value, styleOptions.GraphVersion, styleOptions.OpenApiVersion, styleOptions.OpenApiFormat, graphOpenApi, Response.Body, styleOptions.Style); return new EmptyResult(); } - catch (InvalidOperationException ex) + catch (InvalidOperationException invalidOps) { - return new JsonResult(ex.Message) { StatusCode = StatusCodes.Status400BadRequest }; + _telemetryClient?.TrackException(invalidOps, + _openApiTraceProperties); + + return new JsonResult(invalidOps.Message) { StatusCode = StatusCodes.Status400BadRequest }; } - catch (ArgumentException ex) + catch (ArgumentException argException) { - return new JsonResult(ex.Message) { StatusCode = StatusCodes.Status404NotFound }; + _telemetryClient?.TrackException(argException, + _openApiTraceProperties); + + return new JsonResult(argException.Message) { StatusCode = StatusCodes.Status404NotFound }; } catch (Exception ex) { + _telemetryClient?.TrackException(ex, + _openApiTraceProperties); + return new JsonResult(ex.Message) { StatusCode = StatusCodes.Status500InternalServerError }; } } diff --git a/GraphWebApi/GraphWebApi.csproj b/GraphWebApi/GraphWebApi.csproj index 478ad2e59..9daa4263b 100644 --- a/GraphWebApi/GraphWebApi.csproj +++ b/GraphWebApi/GraphWebApi.csproj @@ -64,7 +64,8 @@ - + + diff --git a/GraphWebApi/Startup.cs b/GraphWebApi/Startup.cs index 0f32faa0e..0ccb62e18 100644 --- a/GraphWebApi/Startup.cs +++ b/GraphWebApi/Startup.cs @@ -24,7 +24,9 @@ using System.Globalization; using Microsoft.AspNetCore.Localization; using Microsoft.Extensions.Options; -using TelemetryService; +using TelemetrySanitizerService; +using OpenAPIService.Interfaces; +using OpenAPIService; namespace GraphWebApi { @@ -56,12 +58,39 @@ public void ConfigureServices(IServiceCollection services) ValidIssuer = Configuration["AzureAd:Issuer"] }; }); + + #region AppInsights + + services.AddApplicationInsightsTelemetry(options => + { + options.InstrumentationKey = Configuration["ApplicationInsights:InstrumentationKey"]; + options.RequestCollectionOptions.InjectResponseHeaders = true; + options.RequestCollectionOptions.TrackExceptions = true; + options.EnableAuthenticationTrackingJavaScript = false; + options.EnableHeartbeat = true; + options.EnableAdaptiveSampling = true; // Control volume of telemetry sent to AppInsights + options.EnableQuickPulseMetricStream = true; // Enable Live Metrics stream + options.EnableDebugLogger = true; + + }); + services.AddApplicationInsightsTelemetryProcessor(); + + if (!_env.IsDevelopment()) + { + services.ConfigureTelemetryModule((module, o) => + module.AuthenticationApiKey = Configuration["ApplicationInsights:AppInsightsApiKey"]); + } + + #endregion + services.AddMemoryCache(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddHttpClient(); services.AddControllers().AddNewtonsoftJson(); services.Configure(Configuration); @@ -83,29 +112,6 @@ public void ConfigureServices(IServiceCollection services) options.DefaultRequestCulture = new RequestCulture("en"); options.SupportedCultures = supportedCultures; }); - - #region AppInsights - - services.AddApplicationInsightsTelemetry(options => - { - options.InstrumentationKey = Configuration["ApplicationInsights:InstrumentationKey"]; - options.RequestCollectionOptions.InjectResponseHeaders = true; - options.RequestCollectionOptions.TrackExceptions = true; - options.EnableAuthenticationTrackingJavaScript = false; - options.EnableHeartbeat = true; - options.EnableAdaptiveSampling = true; // Control volume of telemetry sent to AppInsights - options.EnableQuickPulseMetricStream = true; // Enable Live Metrics stream - options.EnableDebugLogger = true; - }); - services.AddApplicationInsightsTelemetryProcessor(); - - if (!_env.IsDevelopment()) - { - services.ConfigureTelemetryModule((module, o) => - module.AuthenticationApiKey = Configuration["ApplicationInsights:AppInsightsApiKey"]); - } - - #endregion } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. @@ -133,6 +139,9 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) var localizationOptions = app.ApplicationServices.GetService>().Value; app.UseRequestLocalization(localizationOptions); + app.ApplicationServices.GetRequiredService(); + app.ApplicationServices.GetRequiredService(); + app.UseEndpoints(endpoints => { endpoints.MapControllerRoute( diff --git a/MSGraphWebApi.sln b/MSGraphWebApi.sln index 1dafa9ed9..95c992d8c 100644 --- a/MSGraphWebApi.sln +++ b/MSGraphWebApi.sln @@ -35,13 +35,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ChangesService", "ChangesSe EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ChangesService.Test", "ChangesService.Test\ChangesService.Test.csproj", "{D6B39205-2CEB-4232-96E3-E0538A8EC0C9}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TelemetryService.Test", "TelemetryService.Test\TelemetryService.Test.csproj", "{3F86F0DF-82CA-4EC2-8FC7-00B7B62A3A0B}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TelemetrySanitizerService.Test", "TelemetryService.Test\TelemetrySanitizerService.Test.csproj", "{3F86F0DF-82CA-4EC2-8FC7-00B7B62A3A0B}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TelemetryService", "TelemetryService\TelemetryService.csproj", "{F0BA9165-34C0-4A76-B42E-0FC274357AF0}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TelemetrySanitizerService", "TelemetryService\TelemetrySanitizerService.csproj", "{F0BA9165-34C0-4A76-B42E-0FC274357AF0}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UtilityService", "UtilityService\UtilityService.csproj", "{C3241050-9B95-49B9-B223-5FACF59F0052}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UtilityService", "UtilityService\UtilityService.csproj", "{C3241050-9B95-49B9-B223-5FACF59F0052}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UtilityService.Test", "UtilityService.Test\UtilityService.Test.csproj", "{0F4A15DC-F4CD-415D-A62D-6B0D95ED25E8}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UtilityService.Test", "UtilityService.Test\UtilityService.Test.csproj", "{0F4A15DC-F4CD-415D-A62D-6B0D95ED25E8}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/OpenAPIService.Test/OpenAPIDocumentCreatorMock.cs b/OpenAPIService.Test/OpenAPIDocumentCreatorMock.cs index 09e73e1cc..b30da4d04 100644 --- a/OpenAPIService.Test/OpenAPIDocumentCreatorMock.cs +++ b/OpenAPIService.Test/OpenAPIDocumentCreatorMock.cs @@ -5,6 +5,8 @@ using Microsoft.OpenApi.Any; using Microsoft.OpenApi.Interfaces; using Microsoft.OpenApi.Models; +using OpenAPIService.Interfaces; +using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -13,9 +15,17 @@ namespace OpenAPIService.Test /// /// Mock class that creates a sample OpenAPI document. /// - public static class OpenAPIDocumentCreatorMock + public class OpenAPIDocumentCreatorMock { private static readonly ConcurrentDictionary _OpenApiDocuments = new ConcurrentDictionary(); + private readonly IOpenApiService _openApiService; + private const string NullValueError = "Value cannot be null"; + + public OpenAPIDocumentCreatorMock(IOpenApiService openApiService) + { + _openApiService = openApiService + ?? throw new ArgumentNullException(nameof(openApiService), $"{ NullValueError }: { nameof(openApiService) }"); + } /// /// Gets an OpenAPI document of Microsoft Graph @@ -24,7 +34,7 @@ public static class OpenAPIDocumentCreatorMock /// The key for the OpenAPI document dictionary. /// Whether to reload the OpenAPI document from source. /// Instance of an OpenApiDocument - public static OpenApiDocument GetGraphOpenApiDocument(string key, bool forceRefresh) + public OpenApiDocument GetGraphOpenApiDocument(string key, bool forceRefresh) { if (!forceRefresh && _OpenApiDocuments.TryGetValue(key, out OpenApiDocument doc)) { @@ -40,7 +50,7 @@ public static OpenApiDocument GetGraphOpenApiDocument(string key, bool forceRefr /// Creates an OpenAPI document. /// /// Instance of an OpenApi document - private static OpenApiDocument CreateOpenApiDocument() + private OpenApiDocument CreateOpenApiDocument() { string applicationJsonMediaType = "application/json"; @@ -715,7 +725,7 @@ private static OpenApiDocument CreateOpenApiDocument() } }; - return OpenApiService.FixReferences(document); + return _openApiService.FixReferences(document); } } } diff --git a/OpenAPIService.Test/OpenAPIServiceShould.cs b/OpenAPIService.Test/OpenAPIServiceShould.cs index b6e416579..9172b6f0e 100644 --- a/OpenAPIService.Test/OpenAPIServiceShould.cs +++ b/OpenAPIService.Test/OpenAPIServiceShould.cs @@ -3,6 +3,7 @@ // ------------------------------------------------------------------------------------------------------------------------------------------------------ using Microsoft.OpenApi.Models; +using OpenAPIService.Interfaces; using System; using System.Linq; using Xunit; @@ -14,11 +15,16 @@ public class OpenAPIServiceShould private const string Title = "Partial Graph API"; private const string GraphVersion = "beta"; private readonly OpenApiDocument _graphBetaSource = null; + private readonly IOpenApiService _openApiService; + private readonly OpenAPIDocumentCreatorMock _openAPIDocumentCreatorMock; public OpenAPIServiceShould() { + _openApiService = new OpenApiService(); + _openAPIDocumentCreatorMock = new OpenAPIDocumentCreatorMock(_openApiService); + // Create OpenAPI document with default OpenApiStyle = Plain - _graphBetaSource = OpenAPIDocumentCreatorMock.GetGraphOpenApiDocument("Beta", false); + _graphBetaSource = _openAPIDocumentCreatorMock.GetGraphOpenApiDocument("Beta", false); } [Fact] @@ -29,19 +35,19 @@ public void FormatPathFunctionsOfStringDataTypesWithSingleQuotationMarks() string operationId_2 = "reports.getTeamsUserActivityUserDetail-a3f1"; OpenApiDocument source = _graphBetaSource; - var predicate_1 = OpenApiService.CreatePredicate(operationIds: operationId_1, + var predicate_1 = _openApiService.CreatePredicate(operationIds: operationId_1, tags: null, url: null, source: source); - var predicate_2 = OpenApiService.CreatePredicate(operationIds: operationId_2, + var predicate_2 = _openApiService.CreatePredicate(operationIds: operationId_2, tags: null, url: null, source: source); // Act - var subsetOpenApiDocument_1 = OpenApiService.CreateFilteredDocument(source, Title, GraphVersion, predicate_1); - var subsetOpenApiDocument_2 = OpenApiService.CreateFilteredDocument(source, Title, GraphVersion, predicate_2); + var subsetOpenApiDocument_1 = _openApiService.CreateFilteredDocument(source, Title, GraphVersion, predicate_1); + var subsetOpenApiDocument_2 = _openApiService.CreateFilteredDocument(source, Title, GraphVersion, predicate_2); // Assert Assert.Collection(subsetOpenApiDocument_1.Paths, @@ -72,8 +78,8 @@ public void ThrowsInvalidOperationExceptionInCreatePredicateWhenInvalidNumberOfA string.IsNullOrEmpty(tags) && string.IsNullOrEmpty(url)) { - var message = Assert.Throws(() => - OpenApiService.CreatePredicate(operationIds: operationIds, + var message = Assert.Throws(() => + _openApiService.CreatePredicate(operationIds: operationIds, tags: tags, url: url, source: source)).Message; @@ -81,8 +87,8 @@ public void ThrowsInvalidOperationExceptionInCreatePredicateWhenInvalidNumberOfA } else { - var message = Assert.Throws(() => - OpenApiService.CreatePredicate(operationIds: operationIds, + var message = Assert.Throws(() => + _openApiService.CreatePredicate(operationIds: operationIds, tags: tags, url: url, source: source)).Message; @@ -105,7 +111,7 @@ public void ThrowsArgumentExceptionInCreatePredicateWhenNonExistentUrlArgumentIs OpenApiDocument source = _graphBetaSource; // Act and Assert - var message = Assert.Throws(() => OpenApiService.CreatePredicate(operationIds: null, + var message = Assert.Throws(() => _openApiService.CreatePredicate(operationIds: null, tags: null, url: "/foo", source: source, @@ -119,16 +125,16 @@ public void ThrowsArgumentExceptionInApplyStyleWhenNoPathsAreReturned() // Arrange OpenApiDocument source = _graphBetaSource; - var predicate = OpenApiService.CreatePredicate(operationIds: null, + var predicate = _openApiService.CreatePredicate(operationIds: null, tags: null, url: "/", source: source, graphVersion: GraphVersion); // root path will be non-existent in a PowerShell styled doc. - var subsetOpenApiDocument = OpenApiService.CreateFilteredDocument(source, Title, GraphVersion, predicate); + var subsetOpenApiDocument = _openApiService.CreateFilteredDocument(source, Title, GraphVersion, predicate); // Act & Assert - var message = Assert.Throws(() => OpenApiService.ApplyStyle(OpenApiStyle.PowerShell, subsetOpenApiDocument)).Message; + var message = Assert.Throws(() => _openApiService.ApplyStyle(OpenApiStyle.PowerShell, subsetOpenApiDocument)).Message; Assert.Equal("No paths found for the supplied parameters.", message); } @@ -140,14 +146,14 @@ public void ThrowsArgumentExceptionInCreateFilteredDocumentWhenNonExistentOperat // Arrange OpenApiDocument source = _graphBetaSource; - var predicate = OpenApiService.CreatePredicate(operationIds: operationIds, + var predicate = _openApiService.CreatePredicate(operationIds: operationIds, tags: tags, url: null, source: source); // Act & Assert var message = Assert.Throws(() => - OpenApiService.CreateFilteredDocument(source, Title, GraphVersion, predicate)).Message; + _openApiService.CreateFilteredDocument(source, Title, GraphVersion, predicate)).Message; Assert.Equal("No paths found for the supplied parameters.", message); } @@ -162,7 +168,7 @@ public void ReturnValueInCreatePredicateWhenValidArgumentsAreSpecified(string op OpenApiDocument source = _graphBetaSource; // Act - var predicate = OpenApiService.CreatePredicate(operationIds: operationIds, + var predicate = _openApiService.CreatePredicate(operationIds: operationIds, tags: tags, url: url, source: source, @@ -182,13 +188,13 @@ public void ReturnOpenApiDocumentInCreateFilteredDocumentWhenValidArgumentsAreSp OpenApiDocument source = _graphBetaSource; // Act - var predicate = OpenApiService.CreatePredicate(operationIds: operationIds, + var predicate = _openApiService.CreatePredicate(operationIds: operationIds, tags: tags, url: url, source: source, graphVersion: GraphVersion); - var subsetOpenApiDocument = OpenApiService.CreateFilteredDocument(source, Title, GraphVersion, predicate); + var subsetOpenApiDocument = _openApiService.CreateFilteredDocument(source, Title, GraphVersion, predicate); // Assert Assert.NotNull(subsetOpenApiDocument); @@ -225,15 +231,15 @@ public void ReturnStyledOpenApiDocumentInApplyStyleForAllOpenApiStyles(OpenApiSt OpenApiDocument source = _graphBetaSource; // Act - var predicate = OpenApiService.CreatePredicate(operationIds: null, + var predicate = _openApiService.CreatePredicate(operationIds: null, tags: null, url: url, source: source, graphVersion: GraphVersion); - var subsetOpenApiDocument = OpenApiService.CreateFilteredDocument(source, Title, GraphVersion, predicate); + var subsetOpenApiDocument = _openApiService.CreateFilteredDocument(source, Title, GraphVersion, predicate); - subsetOpenApiDocument = OpenApiService.ApplyStyle(style, subsetOpenApiDocument); + subsetOpenApiDocument = _openApiService.ApplyStyle(style, subsetOpenApiDocument); // Assert if (style == OpenApiStyle.GEAutocomplete || style == OpenApiStyle.Plain) @@ -310,14 +316,14 @@ public void RemoveRootPathFromOpenApiDocumentInApplyStyleForPowerShellOpenApiSty OpenApiDocument source = _graphBetaSource; // Act - var predicate = OpenApiService.CreatePredicate(operationIds: "*", + var predicate = _openApiService.CreatePredicate(operationIds: "*", tags: null, url: null, source: source); // fetch all paths/operations - var subsetOpenApiDocument = OpenApiService.CreateFilteredDocument(source, Title, GraphVersion, predicate); + var subsetOpenApiDocument = _openApiService.CreateFilteredDocument(source, Title, GraphVersion, predicate); - subsetOpenApiDocument = OpenApiService.ApplyStyle(OpenApiStyle.PowerShell, subsetOpenApiDocument); + subsetOpenApiDocument = _openApiService.ApplyStyle(OpenApiStyle.PowerShell, subsetOpenApiDocument); // Assert Assert.False(subsetOpenApiDocument.Paths.ContainsKey("/")); // root path @@ -331,14 +337,14 @@ public void EscapePoundCharacterFromNetworkInterfaceSchemaDescription() var expectedDescription = "Description of the NIC (e.g. Ethernet adapter, Wireless LAN adapter Local Area Connection <#/>, etc.)."; // Act - var predicate = OpenApiService.CreatePredicate(operationIds: null, + var predicate = _openApiService.CreatePredicate(operationIds: null, tags: null, url: "/security/hostSecurityProfiles", source: source, graphVersion: GraphVersion); - var subsetOpenApiDocument = OpenApiService.CreateFilteredDocument(source, Title, GraphVersion, predicate); - subsetOpenApiDocument = OpenApiService.ApplyStyle(OpenApiStyle.PowerShell, subsetOpenApiDocument); + var subsetOpenApiDocument = _openApiService.CreateFilteredDocument(source, Title, GraphVersion, predicate); + subsetOpenApiDocument = _openApiService.ApplyStyle(OpenApiStyle.PowerShell, subsetOpenApiDocument); var parentSchema = subsetOpenApiDocument.Components.Schemas["microsoft.graph.networkInterface"]; var descriptionSchema = parentSchema.Properties["description"]; @@ -356,14 +362,14 @@ public void ResolveActionFunctionOperationIdsForPowerShellStyle(string url, Oper OpenApiDocument source = _graphBetaSource; // Act - var predicate = OpenApiService.CreatePredicate(operationIds: null, + var predicate = _openApiService.CreatePredicate(operationIds: null, tags: null, url: url, source: source, graphVersion: GraphVersion); - var subsetOpenApiDocument = OpenApiService.CreateFilteredDocument(source, Title, GraphVersion, predicate); - subsetOpenApiDocument = OpenApiService.ApplyStyle(OpenApiStyle.PowerShell, subsetOpenApiDocument); + var subsetOpenApiDocument = _openApiService.CreateFilteredDocument(source, Title, GraphVersion, predicate); + subsetOpenApiDocument = _openApiService.ApplyStyle(OpenApiStyle.PowerShell, subsetOpenApiDocument); var operationId = subsetOpenApiDocument.Paths .FirstOrDefault().Value .Operations[operationType] diff --git a/OpenAPIService/Interfaces/IOpenApiService.cs b/OpenAPIService/Interfaces/IOpenApiService.cs new file mode 100644 index 000000000..f337671f2 --- /dev/null +++ b/OpenAPIService/Interfaces/IOpenApiService.cs @@ -0,0 +1,30 @@ +// ------------------------------------------------------------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information. +// ------------------------------------------------------------------------------------------------------------------------------------------------------ + +using Microsoft.OpenApi.Models; +using OpenAPIService.Common; +using System; +using System.IO; +using System.Threading.Tasks; + +namespace OpenAPIService.Interfaces +{ + public interface IOpenApiService + { + OpenApiDocument CreateFilteredDocument(OpenApiDocument source, string title, string graphVersion, Func predicate); + + Func CreatePredicate(string operationIds, string tags, string url, + OpenApiDocument source, string graphVersion = "v1.0", bool forceRefresh = false); + + MemoryStream SerializeOpenApiDocument(OpenApiDocument subset, OpenApiStyleOptions styleOptions); + + Task GetGraphOpenApiDocumentAsync(string graphUri, bool forceRefresh); + + OpenApiDocument ApplyStyle(OpenApiStyle style, OpenApiDocument subsetOpenApiDocument); + + Task ConvertCsdlToOpenApiAsync(Stream csdl); + + OpenApiDocument FixReferences(OpenApiDocument document); + } +} diff --git a/OpenAPIService/OpenApiService.cs b/OpenAPIService/OpenApiService.cs index 2bb74b5cd..425acdbf1 100644 --- a/OpenAPIService/OpenApiService.cs +++ b/OpenAPIService/OpenApiService.cs @@ -21,6 +21,9 @@ using System.Text; using OpenAPIService.Common; using UtilityService; +using Microsoft.ApplicationInsights.DataContracts; +using Microsoft.ApplicationInsights; +using OpenAPIService.Interfaces; namespace OpenAPIService { @@ -32,11 +35,18 @@ public enum OpenApiStyle GEAutocomplete } - public class OpenApiService + public class OpenApiService : IOpenApiService { - private static readonly ConcurrentDictionary _OpenApiDocuments = new ConcurrentDictionary(); + private static readonly ConcurrentDictionary _OpenApiDocuments = new(); private static OpenApiUrlTreeNode _openApiRootNode = OpenApiUrlTreeNode.Create(); + private static readonly Dictionary _openApiTraceProperties = + new() { { UtilityConstants.TelemetryPropertyKey_OpenApi, nameof(OpenApiService)} }; + private readonly TelemetryClient _telemetryClient; + public OpenApiService(TelemetryClient telemetryClient = null) + { + _telemetryClient = telemetryClient; + } /// /// Create partial OpenAPI document based on the provided predicate. /// @@ -45,8 +55,12 @@ public class OpenApiService /// Version of the target Microsoft Graph API. /// A predicate function. /// A partial OpenAPI document. - public static OpenApiDocument CreateFilteredDocument(OpenApiDocument source, string title, string graphVersion, Func predicate) + public OpenApiDocument CreateFilteredDocument(OpenApiDocument source, string title, string graphVersion, Func predicate) { + _telemetryClient?.TrackTrace("Creating subset OpenApi document", + SeverityLevel.Information, + _openApiTraceProperties); + var subset = new OpenApiDocument { Info = new OpenApiInfo() @@ -108,6 +122,10 @@ public static OpenApiDocument CreateFilteredDocument(OpenApiDocument source, str CopyReferences(subset); + _telemetryClient?.TrackTrace("Finished creating subset OpenApi document", + SeverityLevel.Information, + _openApiTraceProperties); + return subset; } @@ -121,9 +139,14 @@ public static OpenApiDocument CreateFilteredDocument(OpenApiDocument source, str /// Version of the target Microsoft Graph API. /// Whether to reload the OpenAPI document from source. /// A predicate. - public static Func CreatePredicate(string operationIds, string tags, string url, + public Func CreatePredicate(string operationIds, string tags, string url, OpenApiDocument source, string graphVersion = "v1.0", bool forceRefresh = false) { + string predicateSource = null; + _telemetryClient?.TrackTrace("Creating predicate", + SeverityLevel.Information, + _openApiTraceProperties); + if (url != null && (operationIds != null || tags != null)) { throw new InvalidOperationException("Cannot filter by url and either operationIds and tags at the same time."); @@ -145,6 +168,8 @@ public static Func CreatePredicate(string operationIds, var operationIdsArray = operationIds.Split(','); predicate = (o) => operationIdsArray.Contains(o.OperationId); } + + predicateSource = $"operationIds: {operationIds}"; } else if (tags != null) { @@ -159,16 +184,39 @@ public static Func CreatePredicate(string operationIds, { predicate = (o) => o.Tags.Any(t => tagsArray.Contains(t.Name)); } + + predicateSource = $"tags: {tags}"; } else if (url != null) { if (forceRefresh) { + _telemetryClient?.TrackTrace($"{nameof(forceRefresh)} requested; creating new OpenApiUrlTreeNode", + SeverityLevel.Information, + _openApiTraceProperties); + _openApiRootNode = CreateOpenApiUrlTreeNode(source, graphVersion); + + _telemetryClient?.TrackTrace("Finished creating new OpenApiUrlTreeNode", + SeverityLevel.Information, + _openApiTraceProperties); } else if (!_openApiRootNode.PathItems.ContainsKey(graphVersion)) { + _openApiTraceProperties.Add(UtilityConstants.TelemetryPropertyKey_SanitizeIgnore, nameof(OpenApiService)); + _telemetryClient?.TrackTrace($"Attaching '{graphVersion}' source document to the OpenApiUrlTreeNode", + SeverityLevel.Information, + _openApiTraceProperties); + _openApiTraceProperties.Remove(UtilityConstants.TelemetryPropertyKey_SanitizeIgnore); + _openApiRootNode.Attach(source, graphVersion); + + _openApiTraceProperties.Add(UtilityConstants.TelemetryPropertyKey_SanitizeIgnore, nameof(OpenApiService)); + _telemetryClient?.TrackTrace($"Finished attaching '{graphVersion}' source document to the OpenApiUrlTreeNode", + SeverityLevel.Information, + _openApiTraceProperties); + _openApiTraceProperties.Remove(UtilityConstants.TelemetryPropertyKey_SanitizeIgnore); + } url = url.BaseUriPath() @@ -185,12 +233,18 @@ public static Func CreatePredicate(string operationIds, string[] operationIdsArray = openApiOps.Select(x => x.OperationId).ToArray(); predicate = (o) => operationIdsArray.Contains(o.OperationId); + + predicateSource = $"url: {url}"; } else { throw new InvalidOperationException("Either operationIds, tags or url need to be specified."); } + _telemetryClient?.TrackTrace($"Finished creating predicate for {predicateSource}", + SeverityLevel.Information, + _openApiTraceProperties); + return predicate; } @@ -214,11 +268,15 @@ private static OpenApiUrlTreeNode CreateOpenApiUrlTreeNode(OpenApiDocument sourc /// the array of from. /// The name of the key for the target operations in the node's PathItems dictionary. /// The array of for a given url path. - private static OpenApiOperation[] GetOpenApiOperations(OpenApiUrlTreeNode rootNode, string relativeUrl, string label) + private OpenApiOperation[] GetOpenApiOperations(OpenApiUrlTreeNode rootNode, string relativeUrl, string label) { - Utils.CheckArgumentNull(rootNode, nameof(rootNode)); - Utils.CheckArgumentNullOrEmpty(relativeUrl, nameof(relativeUrl)); - Utils.CheckArgumentNullOrEmpty(label, nameof(label)); + UtilityFunctions.CheckArgumentNull(rootNode, nameof(rootNode)); + UtilityFunctions.CheckArgumentNullOrEmpty(relativeUrl, nameof(relativeUrl)); + UtilityFunctions.CheckArgumentNullOrEmpty(label, nameof(label)); + + _telemetryClient?.TrackTrace($"Fetching OpenApiOperations for url path '{relativeUrl}' from the OpenApiUrlTreeNode", + SeverityLevel.Information, + _openApiTraceProperties); if (relativeUrl.Equals("/", StringComparison.Ordinal)) { @@ -239,6 +297,7 @@ private static OpenApiOperation[] GetOpenApiOperations(OpenApiUrlTreeNode rootNo * with the corresponding OpenApiUrlTreeNode target child segment. */ int parameterNameOffset = 0; + bool matchFound = false; for (int i = 0; i < urlSegments?.Length; i++) { @@ -295,9 +354,26 @@ private static OpenApiOperation[] GetOpenApiOperations(OpenApiUrlTreeNode rootNo { operations = targetChild.PathItems[label].Operations.Values.ToArray(); } + + matchFound = true; } } + if (matchFound) + { + _telemetryClient?.TrackTrace($"Finished fetching OpenApiOperations for url path '{relativeUrl}' from the OpenApiUrlTreeNode." + + $"Matched path: {targetChild.Path}", + SeverityLevel.Information, + _openApiTraceProperties); + } + else + { + + _telemetryClient?.TrackTrace($"No match found in the OpenApiUrlTreeNode for url '{relativeUrl}'", + SeverityLevel.Information, + _openApiTraceProperties); + } + return operations; } @@ -307,8 +383,12 @@ private static OpenApiOperation[] GetOpenApiOperations(OpenApiUrlTreeNode rootNo /// OpenAPI document. /// The modal object containing the required styling options. /// A memory stream. - public static MemoryStream SerializeOpenApiDocument(OpenApiDocument subset, OpenApiStyleOptions styleOptions) + public MemoryStream SerializeOpenApiDocument(OpenApiDocument subset, OpenApiStyleOptions styleOptions) { + _telemetryClient?.TrackTrace($"Serializing the subset OpenApiDocument document for '{styleOptions.OpenApiFormat}' format", + SeverityLevel.Information, + _openApiTraceProperties); + var stream = new MemoryStream(); var sr = new StreamWriter(stream); OpenApiWriterBase writer; @@ -348,6 +428,11 @@ public static MemoryStream SerializeOpenApiDocument(OpenApiDocument subset, Open } sr.Flush(); stream.Position = 0; + + _telemetryClient?.TrackTrace($"Finished serializing the subset OpenApiDocument document for '{styleOptions.OpenApiFormat}' format", + SeverityLevel.Information, + _openApiTraceProperties); + return stream; } @@ -358,14 +443,24 @@ public static MemoryStream SerializeOpenApiDocument(OpenApiDocument subset, Open /// The uri of the Microsoft Graph metadata doc. /// Whether to reload the OpenAPI document from source. /// A task of the value of an OpenAPI document. - public static async Task GetGraphOpenApiDocumentAsync(string graphUri, bool forceRefresh) + public async Task GetGraphOpenApiDocumentAsync(string graphUri, bool forceRefresh) { var csdlHref = new Uri(graphUri); if (!forceRefresh && _OpenApiDocuments.TryGetValue(csdlHref, out OpenApiDocument doc)) { + _telemetryClient?.TrackTrace("Fetch the OpenApi document from the cache", + SeverityLevel.Information, + _openApiTraceProperties); + return doc; } + _openApiTraceProperties.Add(UtilityConstants.TelemetryPropertyKey_SanitizeIgnore, nameof(OpenApiService)); + _telemetryClient?.TrackTrace($"Fetch the OpenApi document from the source: {graphUri}", + SeverityLevel.Information, + _openApiTraceProperties); + _openApiTraceProperties.Remove(UtilityConstants.TelemetryPropertyKey_SanitizeIgnore); + OpenApiDocument source = await CreateOpenApiDocumentAsync(csdlHref); _OpenApiDocuments[csdlHref] = source; return source; @@ -377,8 +472,12 @@ public static async Task GetGraphOpenApiDocumentAsync(string gr /// The OpenApiStyle value. /// The subset of an OpenAPI document. /// An OpenAPI doc with the respective style applied. - public static OpenApiDocument ApplyStyle(OpenApiStyle style, OpenApiDocument subsetOpenApiDocument) + public OpenApiDocument ApplyStyle(OpenApiStyle style, OpenApiDocument subsetOpenApiDocument) { + _telemetryClient?.TrackTrace($"Applying style for '{style}'", + SeverityLevel.Information, + _openApiTraceProperties); + if (style == OpenApiStyle.GEAutocomplete) { // Clone doc before making changes @@ -427,6 +526,10 @@ public static OpenApiDocument ApplyStyle(OpenApiStyle style, OpenApiDocument sub throw new ArgumentException("No paths found for the supplied parameters."); } + _telemetryClient?.TrackTrace($"Finished applying style for '{style}'", + SeverityLevel.Information, + _openApiTraceProperties); + return subsetOpenApiDocument; } @@ -441,7 +544,7 @@ private static OpenApiDocument Clone(OpenApiDocument subsetOpenApiDocument) return reader.Read(stream, out _); } - private static async Task CreateOpenApiDocumentAsync(Uri csdlHref) + private async Task CreateOpenApiDocumentAsync(Uri csdlHref) { var httpClient = CreateHttpClient(); @@ -457,8 +560,12 @@ private static async Task CreateOpenApiDocumentAsync(Uri csdlHr /// /// The CSDL stream. /// An OpenAPI document. - public static async Task ConvertCsdlToOpenApiAsync(Stream csdl) + public async Task ConvertCsdlToOpenApiAsync(Stream csdl) { + _telemetryClient?.TrackTrace("Converting CSDL stream to an OpenApi document", + SeverityLevel.Information, + _openApiTraceProperties); + using var reader = new StreamReader(csdl); var csdlTxt = await reader.ReadToEndAsync(); var edmModel = CsdlReader.Parse(XElement.Parse(csdlTxt).CreateReader()); @@ -479,10 +586,15 @@ public static async Task ConvertCsdlToOpenApiAsync(Stream csdl) OpenApiDocument document = edmModel.ConvertToOpenApi(settings); document = FixReferences(document); + + _telemetryClient?.TrackTrace("Finished converting CSDL stream to an OpenApi document", + SeverityLevel.Information, + _openApiTraceProperties); + return document; } - public static OpenApiDocument FixReferences(OpenApiDocument document) + public OpenApiDocument FixReferences(OpenApiDocument document) { // This method is only needed because the output of ConvertToOpenApi isn't quite a valid OpenApiDocument instance. // So we write it out, and read it back in again to fix it up. diff --git a/TelemetryService.Test/CustomPIIFilterShould.cs b/TelemetryService.Test/CustomPIIFilterShould.cs index 1618b7409..3d3775087 100644 --- a/TelemetryService.Test/CustomPIIFilterShould.cs +++ b/TelemetryService.Test/CustomPIIFilterShould.cs @@ -3,37 +3,40 @@ // ------------------------------------------------------------------------------------------------------------------------------------------------------- using GraphExplorerPermissionsService.Interfaces; + using Microsoft.ApplicationInsights.DataContracts; +using Microsoft.Extensions.DependencyInjection; using MockTestUtility; +using Moq; using System; -using TelemetryService; -using TelemetryService.Test; + using Xunit; -namespace Telemetry.Test +namespace TelemetrySanitizerService.Test { public class CustomPIIFilterShould { - private readonly CustomPIIFilter _telemetryProcessor; - private readonly IPermissionsStore _permissionsStore; - private const string ConfigFilePath = ".\\TestFiles\\Permissions\\appsettings.json"; + private readonly CustomPIIFilter _telemetryClientProcessor; + private readonly IServiceProviderMock _serviceProviderMock; + private readonly IServiceProvider _serviceProvider; public CustomPIIFilterShould() { - _permissionsStore = PermissionStoreFactoryMock.GetPermissionStore(ConfigFilePath); - _telemetryProcessor = new CustomPIIFilter(new TestProcessorNext(), _permissionsStore); + _serviceProviderMock = new IServiceProviderMock(); + _serviceProvider = _serviceProviderMock.MockServiceProvider(); + _telemetryClientProcessor = new CustomPIIFilter(new TestProcessorNext(), _serviceProvider); } [Fact] public void ThrowsArgumentNullExceptionIfNextPocessorArgumentNull() { - Assert.Throws(() => new CustomPIIFilter(next: null, permissionsStore: _permissionsStore)); + Assert.Throws(() => new CustomPIIFilter(next: null, _serviceProvider)); } [Fact] public void ThrowsArgumentNullExceptionIfPermissionsStoreArgumentNull() { - Assert.Throws(() => new CustomPIIFilter(next: _telemetryProcessor, permissionsStore: null)); + Assert.Throws(() => new CustomPIIFilter(next: _telemetryClientProcessor, null)); } [Theory] @@ -58,7 +61,7 @@ public void RedactNumberFromEventTelemetry(string requestPath, string expectedPa eventTelemetry.Properties.Add("RenderedMessage", renderedMessage); // Act - _telemetryProcessor.Process(eventTelemetry); + _telemetryClientProcessor.Process(eventTelemetry); var expectedMessage = $"HTTP {httpMethod + expectedPath} responded {statusCode} in {elapsed} ms"; // Assert @@ -90,7 +93,7 @@ public void RedactGUIDFromEventTelemetry(string requestPath, string expectedPath eventTelemetry.Properties.Add("RenderedMessage", renderedMessage); // Act - _telemetryProcessor.Process(eventTelemetry); + _telemetryClientProcessor.Process(eventTelemetry); var expectedMessage = $"HTTP {httpMethod + expectedPath} responded {statusCode} in {elapsed} ms"; // Assert @@ -120,7 +123,7 @@ public void RedactEmailFromEventTelemetry(string requestPath, string expectedPat eventTelemetry.Properties.Add("RenderedMessage", renderedMessage); // Act - _telemetryProcessor.Process(eventTelemetry); + _telemetryClientProcessor.Process(eventTelemetry); var expectedMessage = $"HTTP {httpMethod} {expectedPath} responded {statusCode} in {elapsed} ms"; // Assert @@ -150,7 +153,7 @@ public void RedactUsernameFromEventTelemetry(string requestPath, string expected eventTelemetry.Properties.Add("RenderedMessage", renderedMessage); // Act - _telemetryProcessor.Process(eventTelemetry); + _telemetryClientProcessor.Process(eventTelemetry); var expectedMessage = $"HTTP {httpMethod} {expectedPath} responded {statusCode} in {elapsed} ms"; // Assert @@ -180,7 +183,7 @@ public void RedactFirstNameFromEventTelemetry(string requestPath, string expecte eventTelemetry.Properties.Add("RenderedMessage", renderedMessage); // Act - _telemetryProcessor.Process(eventTelemetry); + _telemetryClientProcessor.Process(eventTelemetry); var expectedMessage = $"HTTP {httpMethod} {expectedPath} responded {statusCode} in {elapsed} ms"; // Assert @@ -251,7 +254,7 @@ public void SanitizeRequestTelemetry(string incomingUrl, string expectedUrl) }; // Act - _telemetryProcessor.Process(request); + _telemetryClientProcessor.Process(request); // Assert Assert.Equal(expectedUrl, request.Url.ToString()); @@ -271,7 +274,7 @@ public void RedactPIIFromTraceTelemetry(string incomingMsg, string expectedMsg) }; // Act - _telemetryProcessor.Process(trace); + _telemetryClientProcessor.Process(trace); // Assert Assert.Equal(expectedMsg, trace.Message); diff --git a/TelemetryService.Test/IServiceProviderMock.cs b/TelemetryService.Test/IServiceProviderMock.cs new file mode 100644 index 000000000..5f6abac66 --- /dev/null +++ b/TelemetryService.Test/IServiceProviderMock.cs @@ -0,0 +1,41 @@ +// ------------------------------------------------------------------------------------------------------------------------------------------------------- +// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information. +// ------------------------------------------------------------------------------------------------------------------------------------------------------- + +using GraphExplorerPermissionsService.Interfaces; +using Microsoft.Extensions.DependencyInjection; +using Moq; +using System; + +namespace MockTestUtility +{ + public class IServiceProviderMock + { + private const string ConfigFilePath = ".\\TestFiles\\Permissions\\appsettings.json"; + private readonly IPermissionsStore _permissionsStore; + private readonly Mock _serviceProvider; + private readonly Mock _serviceScope; + private readonly Mock _serviceScopeFactory; + + public IServiceProviderMock() + { + _permissionsStore = PermissionStoreFactoryMock.GetPermissionStore(ConfigFilePath); + _serviceProvider = new Mock(); + _serviceScope = new Mock(); + _serviceScopeFactory = new Mock(); + } + + public IServiceProvider MockServiceProvider() + { + _serviceProvider.Setup(x => x.GetService(typeof(IPermissionsStore))).Returns(_permissionsStore); + + _serviceScope.Setup(x => x.ServiceProvider).Returns(_serviceProvider.Object); + + _serviceScopeFactory.Setup(x => x.CreateScope()).Returns(_serviceScope.Object); + + _serviceProvider.Setup(x => x.GetService(typeof(IServiceScopeFactory))).Returns(_serviceScopeFactory.Object); + + return _serviceProvider.Object; + } + } +} diff --git a/TelemetryService.Test/TelemetryService.Test.csproj b/TelemetryService.Test/TelemetrySanitizerService.Test.csproj similarity index 89% rename from TelemetryService.Test/TelemetryService.Test.csproj rename to TelemetryService.Test/TelemetrySanitizerService.Test.csproj index 9378a1640..227459787 100644 --- a/TelemetryService.Test/TelemetryService.Test.csproj +++ b/TelemetryService.Test/TelemetrySanitizerService.Test.csproj @@ -20,6 +20,7 @@ + @@ -34,7 +35,7 @@ - + diff --git a/TelemetryService.Test/TestProcessorNext.cs b/TelemetryService.Test/TestProcessorNext.cs index 99bdb8b83..6146672e6 100644 --- a/TelemetryService.Test/TestProcessorNext.cs +++ b/TelemetryService.Test/TestProcessorNext.cs @@ -5,7 +5,7 @@ using Microsoft.ApplicationInsights.Channel; using Microsoft.ApplicationInsights.Extensibility; -namespace TelemetryService.Test +namespace TelemetrySanitizerService.Test { public class TestProcessorNext : ITelemetryProcessor { diff --git a/TelemetryService/CustomPIIFilter.cs b/TelemetryService/CustomPIIFilter.cs index df72e23fa..f55da29c3 100644 --- a/TelemetryService/CustomPIIFilter.cs +++ b/TelemetryService/CustomPIIFilter.cs @@ -6,14 +6,14 @@ using Microsoft.ApplicationInsights.Channel; using Microsoft.ApplicationInsights.DataContracts; using Microsoft.ApplicationInsights.Extensibility; +using Microsoft.Extensions.DependencyInjection; using System; using System.Collections.Generic; using System.Text.RegularExpressions; using System.Web; -using UriMatchingService; using UtilityService; -namespace TelemetryService +namespace TelemetrySanitizerService { /// @@ -22,7 +22,7 @@ namespace TelemetryService public class CustomPIIFilter : ITelemetryProcessor { private readonly ITelemetryProcessor _next; - private readonly UriTemplateMatcher _uriTemplateMatcher; + private readonly IServiceProvider _serviceProvider; private static readonly Regex _guidRegex = new(@"\b[A-F0-9]{8}(?:-[A-F0-9]{4}){3}-[A-F0-9]{12}\b", RegexOptions.IgnoreCase | RegexOptions.Compiled); @@ -69,17 +69,12 @@ public class CustomPIIFilter : ITelemetryProcessor private const string ODataFilterOperator = "$filter"; private const string SecurityMask = "****"; - public CustomPIIFilter(ITelemetryProcessor next, IPermissionsStore permissionsStore) + public CustomPIIFilter(ITelemetryProcessor next, IServiceProvider serviceProvider) { _next = next ?? throw new ArgumentNullException(nameof(next), $"{ next }: { nameof(next) }"); - - if (permissionsStore == null) - { - throw new ArgumentNullException(nameof(permissionsStore), $"{ permissionsStore }: { nameof(permissionsStore) }"); - } - - _uriTemplateMatcher = permissionsStore.GetUriTemplateMatcher(); + _serviceProvider = serviceProvider + ?? throw new ArgumentNullException(nameof(serviceProvider), $"{ serviceProvider }: { nameof(serviceProvider) }"); } /// @@ -102,7 +97,14 @@ public void Process(ITelemetry item) if (item is TraceTelemetry trace) { - SanitizeTelemetry(trace: trace); + /* Count properties contain custom + * numerical telemetry not required to + * be sanitized. + */ + if (!trace.Properties.ContainsKey(UtilityConstants.TelemetryPropertyKey_SanitizeIgnore)) + { + SanitizeTelemetry(trace: trace); + } } _next.Process(item); @@ -152,6 +154,10 @@ public void SanitizeTelemetry(EventTelemetry customEvent = null, /// The string url with PII sanitized from its query path. private string SanitizeUrlQueryPath(string url) { + // use the service provider to lazily get an IPermissionsStore instance + var permissionsStore = _serviceProvider.GetRequiredService(); + var uriTemplateMatcher = permissionsStore.GetUriTemplateMatcher(); + const char QueryValSeparator = '&'; var queryPath = url?.Query(); @@ -180,7 +186,8 @@ It will be unfeasible sanitizing values from all query params. var valueSegment = queryValue[valueIndex..]; valueSegment = valueSegment.BaseUriPath() .UriTemplatePathFormat(true); - var resultMatch = _uriTemplateMatcher?.Match(new Uri(valueSegment.ToLowerInvariant(), UriKind.RelativeOrAbsolute)); + + var resultMatch = uriTemplateMatcher?.Match(new Uri(valueSegment.ToLowerInvariant(), UriKind.RelativeOrAbsolute)); if (resultMatch != null) { @@ -313,4 +320,4 @@ private static string RedactSearchableValues(string content) return contents[0] + ODataSearchOperator + searchableContent; } } -} +} \ No newline at end of file diff --git a/TelemetryService/TelemetryService.csproj b/TelemetryService/TelemetrySanitizerService.csproj similarity index 83% rename from TelemetryService/TelemetryService.csproj rename to TelemetryService/TelemetrySanitizerService.csproj index 9ecb378c3..5ecc6d0d1 100644 --- a/TelemetryService/TelemetryService.csproj +++ b/TelemetryService/TelemetrySanitizerService.csproj @@ -6,6 +6,7 @@ + diff --git a/UtilityService.Test/UtilityServiceExtensionsShould.cs b/UtilityService.Test/UtilityExtensionsShould.cs similarity index 99% rename from UtilityService.Test/UtilityServiceExtensionsShould.cs rename to UtilityService.Test/UtilityExtensionsShould.cs index f74796360..970fdf751 100644 --- a/UtilityService.Test/UtilityServiceExtensionsShould.cs +++ b/UtilityService.Test/UtilityExtensionsShould.cs @@ -6,7 +6,7 @@ namespace UtilityService.Test { - public class UtilityServiceExtensionsShould + public class UtilityExtensionsShould { [Theory] [InlineData("", "")] diff --git a/UtilityService/UtilityConstants.cs b/UtilityService/UtilityConstants.cs new file mode 100644 index 000000000..7a60cf216 --- /dev/null +++ b/UtilityService/UtilityConstants.cs @@ -0,0 +1,23 @@ +// ------------------------------------------------------------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information. +// ------------------------------------------------------------------------------------------------------------------------------------------------------ + +using Microsoft.ApplicationInsights; +using System.Collections.Generic; + +namespace UtilityService +{ + /// + /// Provides commonly used reusable constants. + /// + public static class UtilityConstants + { + public const string TelemetryPropertyKey_SanitizeIgnore = "SanitizeIgnore"; + public const string TelemetryPropertyKey_Permissions = "Permissions"; + public const string TelemetryPropertyKey_Samples = "Samples"; + public const string TelemetryPropertyKey_Changes = "Changes"; + public const string TelemetryPropertyKey_OpenApi = "OpenApi"; + public const string TelemetryPropertyKey_Snippets = "Snippets"; + } +} + \ No newline at end of file diff --git a/UtilityService/Extensions.cs b/UtilityService/UtilityExtensions.cs similarity index 98% rename from UtilityService/Extensions.cs rename to UtilityService/UtilityExtensions.cs index 38cd00e6d..39c85b059 100644 --- a/UtilityService/Extensions.cs +++ b/UtilityService/UtilityExtensions.cs @@ -8,9 +8,9 @@ namespace UtilityService { /// - /// Provides common utility functions. + /// Provides commonly used extension methods. /// - public static class Extensions + public static class UtilityExtensions { /// /// Strips out the query path from a uri string. diff --git a/UtilityService/Utils.cs b/UtilityService/UtilityFunctions.cs similarity index 92% rename from UtilityService/Utils.cs rename to UtilityService/UtilityFunctions.cs index 00498c648..4734ddfb0 100644 --- a/UtilityService/Utils.cs +++ b/UtilityService/UtilityFunctions.cs @@ -6,7 +6,10 @@ namespace UtilityService { - public static class Utils + /// + /// Provides commonly used utility functions + /// + public static class UtilityFunctions { /// /// Check whether the input argument value is null or not. diff --git a/UtilityService/UtilityService.csproj b/UtilityService/UtilityService.csproj index f208d303c..96815e252 100644 --- a/UtilityService/UtilityService.csproj +++ b/UtilityService/UtilityService.csproj @@ -4,4 +4,8 @@ net5.0 + + + + From ab215570d923a54b1d4c95614b9d926d19fd29a7 Mon Sep 17 00:00:00 2001 From: Irvine Sunday Date: Tue, 29 Jun 2021 20:15:01 +0300 Subject: [PATCH 02/28] Use null-conditionals when capturing count of enumerables --- GraphWebApi/Controllers/GraphExplorerPermissionsController.cs | 2 +- GraphWebApi/Controllers/GraphExplorerSamplesController.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/GraphWebApi/Controllers/GraphExplorerPermissionsController.cs b/GraphWebApi/Controllers/GraphExplorerPermissionsController.cs index 2a4c11101..016e6d26d 100644 --- a/GraphWebApi/Controllers/GraphExplorerPermissionsController.cs +++ b/GraphWebApi/Controllers/GraphExplorerPermissionsController.cs @@ -70,7 +70,7 @@ public async Task GetPermissionScopes([FromQuery]string scopeType } _permissionsTraceProperties.Add(UtilityConstants.TelemetryPropertyKey_SanitizeIgnore, nameof(GraphExplorerPermissionsController)); - _telemetryClient?.TrackTrace($"Fetched {result.Count} permissions", + _telemetryClient?.TrackTrace($"Fetched {result?.Count ?? 0} permissions", SeverityLevel.Information, _permissionsTraceProperties); diff --git a/GraphWebApi/Controllers/GraphExplorerSamplesController.cs b/GraphWebApi/Controllers/GraphExplorerSamplesController.cs index 13fd56566..75ffd7bcc 100644 --- a/GraphWebApi/Controllers/GraphExplorerSamplesController.cs +++ b/GraphWebApi/Controllers/GraphExplorerSamplesController.cs @@ -72,7 +72,7 @@ public async Task GetSampleQueriesListAsync(string search, string } _samplesTraceProperties.Add(UtilityConstants.TelemetryPropertyKey_SanitizeIgnore, nameof(GraphExplorerSamplesController)); - _telemetryClient?.TrackTrace($"{filteredSampleQueries.Count} sample queries found from search value '{search}'", + _telemetryClient?.TrackTrace($"{filteredSampleQueries?.Count ?? 0} sample queries found from search value '{search}'", SeverityLevel.Information, _samplesTraceProperties); @@ -353,7 +353,7 @@ private async Task FetchSampleQueriesListAsync(string org, st } _samplesTraceProperties.Add(UtilityConstants.TelemetryPropertyKey_SanitizeIgnore, nameof(GraphExplorerSamplesController)); - _telemetryClient?.TrackTrace($"Fetched {sampleQueriesList?.SampleQueries.Count} samples", + _telemetryClient?.TrackTrace($"Fetched {sampleQueriesList?.SampleQueries?.Count ?? 0} samples", SeverityLevel.Information, _samplesTraceProperties); From 8d8da952f2d3fc57c1290c51c825c7c16a047d40 Mon Sep 17 00:00:00 2001 From: Irvine Sunday Date: Wed, 30 Jun 2021 15:29:37 +0300 Subject: [PATCH 03/28] Subtle change to trigger build --- GraphWebApi/Controllers/GraphExplorerSamplesController.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/GraphWebApi/Controllers/GraphExplorerSamplesController.cs b/GraphWebApi/Controllers/GraphExplorerSamplesController.cs index 75ffd7bcc..2b7eb64b7 100644 --- a/GraphWebApi/Controllers/GraphExplorerSamplesController.cs +++ b/GraphWebApi/Controllers/GraphExplorerSamplesController.cs @@ -1,6 +1,6 @@ -// ------------------------------------------------------------------------------------------------------------------------------------------------------ +// ------------------------------------------------------------------------------------------------------------------------------------------------------- // Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information. -// ------------------------------------------------------------------------------------------------------------------------------------------------------ +// ------------------------------------------------------------------------------------------------------------------------------------------------------- using System; using Microsoft.AspNetCore.Mvc; From 39f44e7dc25ba95867c60ca58ea08400302d1e60 Mon Sep 17 00:00:00 2001 From: Irvine Sunday Date: Thu, 1 Jul 2021 17:27:04 +0300 Subject: [PATCH 04/28] Revert metadata source The new metadata with annotations has some annotation discrepancies that needs to be looked into before being accepted into prod. --- GraphWebApi/appsettings.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/GraphWebApi/appsettings.json b/GraphWebApi/appsettings.json index b349e66a9..bbca34d4a 100644 --- a/GraphWebApi/appsettings.json +++ b/GraphWebApi/appsettings.json @@ -16,8 +16,8 @@ }, "AllowedHosts": "*", "GraphMetadata": { - "V1.0": "https://raw.githubusercontent.com/microsoftgraph/msgraph-metadata/master/clean_v10_metadata/cleanMetadataWithDescriptionsAndAnnotationsv1.0.xml", - "Beta": "https://raw.githubusercontent.com/microsoftgraph/msgraph-metadata/master/clean_beta_metadata/cleanMetadataWithDescriptionsAndAnnotationsbeta.xml" + "V1.0": "https://raw.githubusercontent.com/microsoftgraph/msgraph-metadata/master/clean_v10_metadata/cleanMetadataWithDescriptionsv1.0.xml", + "Beta": "https://raw.githubusercontent.com/microsoftgraph/msgraph-metadata/master/clean_beta_metadata/cleanMetadataWithDescriptionsbeta.xml" }, "BlobStorage": { "AzureConnectionString": "ENTER_AZURE_STORAGE_CONNECTION_STRING", From a49e17a07dfa4129bd374f49826aa33b534cfa22 Mon Sep 17 00:00:00 2001 From: Irvine Sunday <40403681+irvinesunday@users.noreply.github.com> Date: Mon, 5 Jul 2021 10:50:39 +0300 Subject: [PATCH 05/28] Merges dev into master (#617) (#626) * Add telemetry diagnostic tracing for samples * Add trace info for permissions * Update telemetry trace capture points * Install Application insights for trace telemetry tracking * Adds trace messages to log diagnostic events at service level * Track exceptions including stack traces at controller level * Add Telemetry lib. to OpenAPIService * Add OpenApiService as Singleton * Use static property to set TelemetryClient * Use parameter to set telemetry client in OpenApiService * Adds the ChangesService class as a singleton on startup and refactor code * Modify the ChangesService from a static class to a singleton and add trace telemetry for tracking purposes * Update value of the trace properties dictionary * Initialize a telemetry client and use it to track exceptions * Use a static property to set the telemetry client and use it to track trace telemetry * Install ApplicationInsights * Revert "Adds the ChangesService class as a singleton on startup and refactor code" This reverts commit 36bb74af07c2dd5f7351286860a033daab59730d. * Revert "Modify the ChangesService from a static class to a singleton and add trace telemetry for tracking purposes" This reverts commit 71eeb6b9e659c9e2e60af1de0876cc668953d337. * Reverts changes for ChangesService class and adds trace telemetry * Edit value for the trace properties dictionary * Remove unnecessary line * Remove whitespaces * Add null check * Track exceptions thrown for better inspection * Update OpenApiService and controller trace information and properties * Message update * Rename the trace properties dictionaries * Further trace telemetry message and property updates * Add log messages to track trace events and exceptions * Install Application Insights * Edit the trace message * Add count properties and prevent them from being sanitized * Minor edit * Create UtilityConstants class that will have constants * Add UtilityService project dependencies * Use UtilityService constants * Add TelemetryClientUtility class and update its implementations Also, some class renames and code simplifications. Remove unnecessary code as well. * Code formatting * Transfer TelemetryClientUtility to a dedicated library; delete unnecessary packages and code * Remove unnecessary code and imports; code simplification * Auto stash before merge of "feature/mk/instrument-changes-service" and "origin/feature/is/code-instrumentation" * Add a reference to the TelemetryClientWrapper * Add a reference for the TelemetryClientWrapper * Use a singleton instance of telemetry client to track telemetry across multiple services and controllers * Add a Changes property key for storing count data to avoid unnecessary numerical sanitization * Clean up file * Add a trace message to trigger an alert when our permission data source is empty * Fix indentation * Remove whitespace * Create a joining service. * Refactor startup file * Inject IServiceProvider and use it to get an IPermissionsStore instance * Use the TelemetryClient available from our DI container to track telemetry * Remove optional param * Add/remove packages * Mock an IServiceProvider object and use it to call a mocked instance of IPermissionsStore * Simplify null check * Remove files * Create interfaces and initialize the services as singletons * Implement the interface * Add OpenApi telemetry property key as a constant * Replace string with changes telemetry property key constant * Add project reference * Code cleanup * Add property to prevent graph uri Version from sanitization * Add abstract methods to interfaces and refactor code * Code cleanup * Rename dictionary value to PermissionsStore and delete the dictionary afterwards * Rename dictionary value * Use nameof() operator to get the class names * Use defensive programming Co-authored-by: Irvine Sunday Co-authored-by: Vincent Biret Co-authored-by: George Co-authored-by: Maggie Kimani Co-authored-by: Irvine Sunday Co-authored-by: Vincent Biret Co-authored-by: George --- GraphWebApi/Controllers/GraphExplorerSamplesController.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/GraphWebApi/Controllers/GraphExplorerSamplesController.cs b/GraphWebApi/Controllers/GraphExplorerSamplesController.cs index 2b7eb64b7..75ffd7bcc 100644 --- a/GraphWebApi/Controllers/GraphExplorerSamplesController.cs +++ b/GraphWebApi/Controllers/GraphExplorerSamplesController.cs @@ -1,6 +1,6 @@ -// ------------------------------------------------------------------------------------------------------------------------------------------------------- +// ------------------------------------------------------------------------------------------------------------------------------------------------------ // Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information. -// ------------------------------------------------------------------------------------------------------------------------------------------------------- +// ------------------------------------------------------------------------------------------------------------------------------------------------------ using System; using Microsoft.AspNetCore.Mvc; From 357bd809ccc091a7325cd9041aaeb9349e91bc96 Mon Sep 17 00:00:00 2001 From: Irvine Sunday <40403681+irvinesunday@users.noreply.github.com> Date: Mon, 5 Jul 2021 19:47:03 +0300 Subject: [PATCH 06/28] Fix: Revert explode value to false in operation parameters (#621) * Add test for validating setting explode property * Visit Parameters to set Explode property to false * Add parameters to test * Move parameter visitor to separate class to update entire OpenApi doc. * Nit fix on comment * Use instance variable to make calls to the service * Update OpenAPIService/OperationSearch.cs Co-authored-by: Vincent Biret * Update OpenAPIService/OperationSearch.cs Co-authored-by: Vincent Biret * Update OpenAPIService/OperationSearch.cs Co-authored-by: Vincent Biret * Update OpenAPIService/OperationSearch.cs Co-authored-by: Vincent Biret * Update OpenAPIService.Test/OpenAPIDocumentCreatorMock.cs Co-authored-by: Vincent Biret * Update OpenAPIService.Test/OpenAPIDocumentCreatorMock.cs Co-authored-by: Vincent Biret * Update OpenAPIService/OperationSearch.cs Co-authored-by: Vincent Biret * Auto stash before merge of "fix/is/set-explode-false" and "microsoft-graph-devx-api/fix/is/set-explode-false" * Refactor class; import LINQ library * Update OpenAPIService/OperationSearch.cs Co-authored-by: Vincent Biret * Add default locale for requests with unspecified Accept-Language Unrelated to the overall work item but is important for servicing /sample requests which don't provide an Accept-Language value in the header. Co-authored-by: Irvine Sunday Co-authored-by: Vincent Biret --- .../GraphExplorerSamplesController.cs | 2 +- .../OpenAPIDocumentCreatorMock.cs | 93 ++++++++++--------- OpenAPIService.Test/OpenAPIServiceShould.cs | 28 ++++++ OpenAPIService/OperationSearch.cs | 44 +++++++-- OpenAPIService/PowershellFormatter.cs | 2 +- 5 files changed, 114 insertions(+), 55 deletions(-) diff --git a/GraphWebApi/Controllers/GraphExplorerSamplesController.cs b/GraphWebApi/Controllers/GraphExplorerSamplesController.cs index 75ffd7bcc..65f562e51 100644 --- a/GraphWebApi/Controllers/GraphExplorerSamplesController.cs +++ b/GraphWebApi/Controllers/GraphExplorerSamplesController.cs @@ -337,7 +337,7 @@ public async Task DeleteSampleQueryAsync(string id) /// A list of Sample Queries. private async Task FetchSampleQueriesListAsync(string org, string branchName) { - string locale = RequestHelper.GetPreferredLocaleLanguage(Request); + string locale = RequestHelper.GetPreferredLocaleLanguage(Request) ?? Constants.DefaultLocale; _telemetryClient?.TrackTrace($"Request to fetch samples for locale '{locale}'", SeverityLevel.Information, _samplesTraceProperties); diff --git a/OpenAPIService.Test/OpenAPIDocumentCreatorMock.cs b/OpenAPIService.Test/OpenAPIDocumentCreatorMock.cs index b30da4d04..1bb5a52bb 100644 --- a/OpenAPIService.Test/OpenAPIDocumentCreatorMock.cs +++ b/OpenAPIService.Test/OpenAPIDocumentCreatorMock.cs @@ -352,6 +352,21 @@ private OpenApiDocument CreateOpenApiDocument() OperationId = "users.GetMessages", Summary = "Get messages from users", Description = "The messages in a mailbox or folder. Read-only. Nullable.", + Parameters = new List + { + new OpenApiParameter() + { + Name = "$select", + In = ParameterLocation.Query, + Required = true, + Description = "Select properties to be returned", + Schema = new OpenApiSchema() + { + Type = "array" + } + // missing explode parameter + } + }, Responses = new OpenApiResponses() { { @@ -563,22 +578,20 @@ private OpenApiDocument CreateOpenApiDocument() Summary = "Invoke action keepAlive", Parameters = new List { + new OpenApiParameter() { - new OpenApiParameter() + Name = "call-id", + In = ParameterLocation.Path, + Description = "key: id of call", + Required = true, + Schema = new OpenApiSchema() + { + Type = "string" + }, + Extensions = new Dictionary { - Name = "call-id", - In = ParameterLocation.Path, - Description = "key: id of call", - Required = true, - Schema = new OpenApiSchema() - { - Type = "string" - }, - Extensions = new Dictionary { - { - "x-ms-docs-key-type", new OpenApiString("call") - } + "x-ms-docs-key-type", new OpenApiString("call") } } } @@ -611,52 +624,46 @@ private OpenApiDocument CreateOpenApiDocument() { Tags = new List { + new OpenApiTag() { - new OpenApiTag() - { - Name = "groups.Functions" - } + Name = "groups.Functions" } }, OperationId = "groups.group.events.event.calendar.events.delta", Summary = "Invoke function delta", Parameters = new List { + new OpenApiParameter() { - new OpenApiParameter() + Name = "group-id", + In = ParameterLocation.Path, + Description = "key: id of group", + Required = true, + Schema = new OpenApiSchema() + { + Type = "string" + }, + Extensions = new Dictionary { - Name = "group-id", - In = ParameterLocation.Path, - Description = "key: id of group", - Required = true, - Schema = new OpenApiSchema() - { - Type = "string" - }, - Extensions = new Dictionary { - { - "x-ms-docs-key-type", new OpenApiString("group") - } + "x-ms-docs-key-type", new OpenApiString("group") } } }, + new OpenApiParameter() { - new OpenApiParameter() + Name = "event-id", + In = ParameterLocation.Path, + Description = "key: id of event", + Required = true, + Schema = new OpenApiSchema() + { + Type = "string" + }, + Extensions = new Dictionary { - Name = "event-id", - In = ParameterLocation.Path, - Description = "key: id of event", - Required = true, - Schema = new OpenApiSchema() - { - Type = "string" - }, - Extensions = new Dictionary { - { - "x-ms-docs-key-type", new OpenApiString("event") - } + "x-ms-docs-key-type", new OpenApiString("event") } } } diff --git a/OpenAPIService.Test/OpenAPIServiceShould.cs b/OpenAPIService.Test/OpenAPIServiceShould.cs index 9172b6f0e..6d22204a5 100644 --- a/OpenAPIService.Test/OpenAPIServiceShould.cs +++ b/OpenAPIService.Test/OpenAPIServiceShould.cs @@ -378,5 +378,33 @@ public void ResolveActionFunctionOperationIdsForPowerShellStyle(string url, Oper // Assert Assert.Equal(expectedOperationId, operationId); } + + [Theory] + [InlineData(OpenApiStyle.GEAutocomplete)] + [InlineData(OpenApiStyle.Plain)] + [InlineData(OpenApiStyle.PowerPlatform)] + [InlineData(OpenApiStyle.PowerShell)] + public void SetExplodePropertyToFalseInParametersWithStyleEqualsFormForAllStyles(OpenApiStyle style) + { + // Arrange + OpenApiDocument source = _graphBetaSource; + + // Act + var predicate = _openApiService.CreatePredicate(operationIds: null, + tags: null, + url: "/users/{user-id}/messages/{message-id}", + source: source, + graphVersion: GraphVersion); + + var subsetOpenApiDocument = _openApiService.CreateFilteredDocument(source, Title, GraphVersion, predicate); + subsetOpenApiDocument = _openApiService.ApplyStyle(style, subsetOpenApiDocument); + + var parameter = subsetOpenApiDocument.Paths + .FirstOrDefault().Value + .Operations.FirstOrDefault().Value + .Parameters.First(s => s.Name.Equals("$select")); + // Assert + Assert.False(parameter.Explode); + } } } diff --git a/OpenAPIService/OperationSearch.cs b/OpenAPIService/OperationSearch.cs index f1159419c..0af0a7d5d 100644 --- a/OpenAPIService/OperationSearch.cs +++ b/OpenAPIService/OperationSearch.cs @@ -1,23 +1,31 @@ -using Microsoft.OpenApi.Models; +// ------------------------------------------------------------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information. +// ------------------------------------------------------------------------------------------------------------------------------------------------------ + +using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Services; using System; using System.Collections.Generic; +using System.Linq; namespace OpenAPIService { public class OperationSearch : OpenApiVisitorBase { private readonly Func _predicate; + private readonly List _searchResults = new(); - private List _searchResults = new List(); - - public IList SearchResults { get { return _searchResults; } } + public IList SearchResults => _searchResults; public OperationSearch(Func predicate) { - this._predicate = predicate; + _predicate = predicate ?? throw new ArgumentNullException(nameof(predicate)); } + /// + /// Visits . + /// + /// The target . public override void Visit(OpenApiOperation operation) { if (_predicate(operation)) @@ -30,16 +38,32 @@ public override void Visit(OpenApiOperation operation) } } - private CurrentKeys CopyCurrentKeys(CurrentKeys currentKeys) + /// + /// Visits list of . + /// + /// The target list of . + public override void Visit(IList parameters) + { + /* The Parameter.Explode property should be true + * if Parameter.Style == Form; but OData query params + * as used in Microsoft Graph implement explode: false + * ex: $select=id,displayName,givenName + */ + foreach (var parameter in parameters.Where(x => x.Style == ParameterStyle.Form)) + { + parameter.Explode = false; + } + + base.Visit(parameters); + } + + private static CurrentKeys CopyCurrentKeys(CurrentKeys currentKeys) { - var keys = new CurrentKeys + return new CurrentKeys { Path = currentKeys.Path, Operation = currentKeys.Operation }; - - return keys; } } - } diff --git a/OpenAPIService/PowershellFormatter.cs b/OpenAPIService/PowershellFormatter.cs index a13337cb0..3903abff7 100644 --- a/OpenAPIService/PowershellFormatter.cs +++ b/OpenAPIService/PowershellFormatter.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------------------------------------------------------------------------------------ +// ------------------------------------------------------------------------------------------------------------------------------------------------------ // Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information. // ------------------------------------------------------------------------------------------------------------------------------------------------------ From 1b0eb8b1dfd47c3edf3db99bdaf351699329bb8e Mon Sep 17 00:00:00 2001 From: Maggie Kimani Date: Wed, 7 Jul 2021 15:19:27 +0300 Subject: [PATCH 07/28] Update PermissionsStore class (#622) * Update how permissions values are fetched * Update path to the new permissions files container * Update test files structure * Update file path * Make linq query more readable * Refactor code Co-authored-by: Irvine Sunday Co-authored-by: Irvine Sunday <40403681+irvinesunday@users.noreply.github.com> --- .../Services/PermissionsStore.cs | 23 ++- .../permissions-test-file-v1.0_ver1.json | 137 ++++++++---------- .../permissions-test-file-v1.0_ver2.json | 61 ++++---- 3 files changed, 106 insertions(+), 115 deletions(-) diff --git a/GraphExplorerPermissionsService/Services/PermissionsStore.cs b/GraphExplorerPermissionsService/Services/PermissionsStore.cs index a1bfe8730..17528b961 100644 --- a/GraphExplorerPermissionsService/Services/PermissionsStore.cs +++ b/GraphExplorerPermissionsService/Services/PermissionsStore.cs @@ -397,13 +397,22 @@ public async Task> GetScopesAsync(string scopeType = "Del return null; } - JArray resultValue = new JArray(); - resultValue = (JArray)_scopesListTable[int.Parse(resultMatch.Key)]; - - var scopes = resultValue.FirstOrDefault(x => x.Value("HttpVerb") == method)? - .SelectToken(scopeType)? - .Select(s => (string)s) - .ToArray(); + string[] scopes = null; + var resultValue = _scopesListTable[int.Parse(resultMatch.Key)] as JToken; + foreach (JProperty httpVerb in resultValue) + { + if (httpVerb.Name.Equals(method, StringComparison.OrdinalIgnoreCase)) + { + foreach (JProperty scopeCategory in httpVerb.Value) + { + if (scopeCategory.Name.Equals(scopeType, StringComparison.OrdinalIgnoreCase)) + { + scopes = scopeCategory.Value?.ToObject(); + break; + } + } + } + } if (scopes == null) { diff --git a/PermissionsService.Test/TestFiles/permissions-test-file-v1.0_ver1.json b/PermissionsService.Test/TestFiles/permissions-test-file-v1.0_ver1.json index d8c2da905..6a1d236a7 100644 --- a/PermissionsService.Test/TestFiles/permissions-test-file-v1.0_ver1.json +++ b/PermissionsService.Test/TestFiles/permissions-test-file-v1.0_ver1.json @@ -1,8 +1,7 @@ { "ApiPermissions": { - "/security/alerts/{id}": [ - { - "HttpVerb": "GET", + "/security/alerts/{id}": { + "GET": { "DelegatedWork": [ "SecurityEvents.Read.All", "SecurityEvents.ReadWrite.All" @@ -13,8 +12,7 @@ "SecurityEvents.ReadWrite.All" ] }, - { - "HttpVerb": "PATCH", + "PATCH": { "DelegatedWork": [ "SecurityEvents.ReadWrite.All" ], @@ -23,10 +21,10 @@ "SecurityEvents.ReadWrite.All" ] } - ], - "/security/alerts": [ - { - "HttpVerb": "GET", + }, + "/security/alerts": { + + "GET": { "DelegatedWork": [ "SecurityEvents.Read.All", "SecurityEvents.ReadWrite.All" @@ -37,10 +35,10 @@ "SecurityEvents.ReadWrite.All" ] } - ], - "/me/calendars/{id}": [ - { - "HttpVerb": "DELETE", + }, + "/me/calendars/{id}": { + + "DELETE": { "DelegatedWork": [ "Calendars.ReadWrite" ], @@ -51,8 +49,8 @@ "Calendars.ReadWrite" ] }, - { - "HttpVerb": "GET", + + "GET": { "DelegatedWork": [ "Calendars.Read" ], @@ -63,8 +61,8 @@ "Calendars.Read" ] }, - { - "HttpVerb": "PATCH", + + "PATCH": { "DelegatedWork": [ "Calendars.ReadWrite" ], @@ -75,10 +73,12 @@ "Calendars.ReadWrite" ] } - ], - "/users/{id}/calendars/{id}": [ - { - "HttpVerb": "DELETE", + + }, + "/users/{id}/calendars/{id}": { + + + "DELETE": { "DelegatedWork": [ "Calendars.ReadWrite" ], @@ -88,9 +88,10 @@ "Application": [ "Calendars.ReadWrite" ] + }, - { - "HttpVerb": "GET", + + "GET": { "DelegatedWork": [ "Calendars.Read" ], @@ -101,8 +102,8 @@ "Calendars.Read" ] }, - { - "HttpVerb": "PATCH", + + "PATCH": { "DelegatedWork": [ "Calendars.ReadWrite" ], @@ -113,10 +114,10 @@ "Calendars.ReadWrite" ] } - ], - "/me/calendars/{id}": [ - { - "HttpVerb": "DELETE", + }, + "/me/calendars/{id}": { + + "DELETE": { "DelegatedWork": [ "Calendars.ReadWrite" ], @@ -127,8 +128,8 @@ "Calendars.ReadWrite" ] }, - { - "HttpVerb": "GET", + + "GET": { "DelegatedWork": [ "Calendars.Read" ], @@ -139,8 +140,7 @@ "Calendars.Read" ] }, - { - "HttpVerb": "PATCH", + "PATCH": { "DelegatedWork": [ "Calendars.ReadWrite" ], @@ -151,10 +151,9 @@ "Calendars.ReadWrite" ] } - ], - "/me/calendargroup/calendars/{id}": [ - { - "HttpVerb": "DELETE", + }, + "/me/calendargroup/calendars/{id}": { + "DELETE": { "DelegatedWork": [ "Calendars.ReadWrite" ], @@ -165,8 +164,7 @@ "Calendars.ReadWrite" ] }, - { - "HttpVerb": "GET", + "GET": { "DelegatedWork": [ "Calendars.Read" ], @@ -177,8 +175,7 @@ "Calendars.Read" ] }, - { - "HttpVerb": "PATCH", + "PATCH": { "DelegatedWork": [ "Calendars.ReadWrite" ], @@ -189,10 +186,9 @@ "Calendars.ReadWrite" ] } - ], - "/users/{id}/calendargroup/calendars": [ - { - "HttpVerb": "GET", + }, + "/users/{id}/calendargroup/calendars": { + "GET": { "DelegatedWork": [ "Calendars.Read" ], @@ -202,9 +198,9 @@ "Application": [ "Calendars.Read" ] + }, - { - "HttpVerb": "POST", + "POST": { "DelegatedWork": [ "Calendars.ReadWrite" ], @@ -215,10 +211,9 @@ "Calendars.ReadWrite" ] } - ], - "/me/calendargroup/calendars/{id}": [ - { - "HttpVerb": "DELETE", + }, + "/me/calendargroup/calendars/{id}": { + "DELETE": { "DelegatedWork": [ "Calendars.ReadWrite" ], @@ -229,8 +224,8 @@ "Calendars.ReadWrite" ] }, - { - "HttpVerb": "GET", + + "GET": { "DelegatedWork": [ "Calendars.Read" ], @@ -241,8 +236,7 @@ "Calendars.Read" ] }, - { - "HttpVerb": "PATCH", + "PATCH": { "DelegatedWork": [ "Calendars.ReadWrite" ], @@ -253,38 +247,35 @@ "Calendars.ReadWrite" ] } - ], - "/workbook/worksheets/{id}/charts/{id}": [ - { - "HttpVerb": "GET", + }, + "/workbook/worksheets/{id}/charts/{id}": { + "GET": { "DelegatedWork": [ "Files.ReadWrite" ], "DelegatedPersonal": [], "Application": [] }, - { - "HttpVerb": "PATCH", + "PATCH": { "DelegatedWork": [ "Files.ReadWrite" ], "DelegatedPersonal": [], "Application": [] } - ], - "/workbook/worksheets/{id}/charts/{id}/image(width=640)": [ - { - "HttpVerb": "GET", + + }, + "/workbook/worksheets/{id}/charts/{id}/image(width=640)": { + "GET": { "DelegatedWork": [ "Files.ReadWrite" ], "DelegatedPersonal": [], "Application": [] } - ], - "/me/photo": [ - { - "HttpVerb": "GET", + }, + "/me/photo": { + "GET": { "DelegatedWork": [ "User.ReadBasic.All", "User.Read", @@ -299,8 +290,7 @@ "Not supported." ] }, - { - "HttpVerb": "PATCH", + "PATCH": { "DelegatedWork": [ "User.ReadWrite", "User.ReadWrite.All" @@ -312,8 +302,7 @@ "Not supported." ] }, - { - "HttpVerb": "PUT", + "PUT": { "DelegatedWork": [ "User.ReadWrite", "User.ReadWrite.All" @@ -325,6 +314,6 @@ "Not supported." ] } - ] + } } } \ No newline at end of file diff --git a/PermissionsService.Test/TestFiles/permissions-test-file-v1.0_ver2.json b/PermissionsService.Test/TestFiles/permissions-test-file-v1.0_ver2.json index 14b9db9eb..39e96989a 100644 --- a/PermissionsService.Test/TestFiles/permissions-test-file-v1.0_ver2.json +++ b/PermissionsService.Test/TestFiles/permissions-test-file-v1.0_ver2.json @@ -1,8 +1,7 @@ { "ApiPermissions": { - "/accessreviews('{id}')/reviewers": [ - { - "HttpVerb": "POST", + "/accessreviews('{id}')/reviewers": { + "POST": { "DelegatedWork": [ "AccessReview.ReadWrite.All" ], @@ -11,8 +10,8 @@ "AccessReview.ReadWrite.Membership" ] }, - { - "HttpVerb": "GET", + + "GET": { "DelegatedWork": [ "AccessReview.Read.All", "AccessReview.ReadWrite.All" @@ -23,10 +22,9 @@ "AccessReview.ReadWrite.Membership" ] } - ], - "/accessreviews('')/applydecisions()": [ - { - "HttpVerb": "POST", + }, + "/accessreviews('')/applydecisions()": { + "POST": { "DelegatedWork": [ "AccessReview.ReadWrite.All" ], @@ -35,10 +33,11 @@ "AccessReview.ReadWrite.Membership" ] } - ], - "/administrativeunits/{id}": [ - { - "HttpVerb": "DELETE", + + }, + "/administrativeunits/{id}": { + + "DELETE": { "DelegatedWork": [ "AdministrativeUnit.ReadWrite.All", "Directory.AccessAsUser.All" @@ -48,8 +47,7 @@ "AdministrativeUnit.ReadWrite.All" ] }, - { - "HttpVerb": "GET", + "GET": { "DelegatedWork": [ "AdministrativeUnit.Read.All", "Directory.Read.All", @@ -65,8 +63,7 @@ "Directory.ReadWrite.All" ] }, - { - "HttpVerb": "PATCH", + "PATCH": { "DelegatedWork": [ "AdministrativeUnit.ReadWrite.All", "Directory.AccessAsUser.All" @@ -76,10 +73,9 @@ "AdministrativeUnit.ReadWrite.All" ] } - ], - "/administrativeunits/{id}/members/{id}": [ - { - "HttpVerb": "GET", + }, + "/administrativeunits/{id}/members/{id}": { + "GET": { "DelegatedWork": [ "AdministrativeUnit.Read.All", "Directory.Read.All", @@ -95,10 +91,9 @@ "Directory.ReadWrite.All" ] } - ], - "/security/alerts/{id}": [ - { - "HttpVerb": "GET", + }, + "/security/alerts/{id}": { + "GET": { "DelegatedWork": [ "SecurityEvents.Read.All", "SecurityEvents.ReadWrite.All" @@ -109,10 +104,9 @@ "SecurityEvents.ReadWrite.All" ] } - ], - "/anonymousipriskevents/{id}": [ - { - "HttpVerb": "GET", + }, + "/anonymousipriskevents/{id}": { + "GET": { "DelegatedWork": [ "IdentityRiskEvent.Read.All" ], @@ -121,10 +115,9 @@ "IdentityRiskEvent.Read.All" ] } - ], - "/lorem/ipsum/{id}": [ - { - "HttpVerb": "GET", + }, + "/lorem/ipsum/{id}": { + "GET": { "DelegatedWork": [ "LoremIpsum.Read.All", "LoremIpsum.ReadWrite.All" @@ -135,6 +128,6 @@ "LoremIpsum.ReadWrite.All" ] } - ] + } } } \ No newline at end of file From ed4b4c19645a9a49c1da163963078e7ac445ae76 Mon Sep 17 00:00:00 2001 From: Charles Wahome Date: Fri, 9 Jul 2021 14:48:32 +0300 Subject: [PATCH 08/28] Task: add GitHub templates (#634) * add issue creation templates * add pull request template --- .github/ISSUE_TEMPLATE/bug_report.md | 38 +++++++++++++++++++++++ .github/ISSUE_TEMPLATE/feature_request.md | 20 ++++++++++++ .github/pull_request_template.md | 20 ++++++++++++ 3 files changed, 78 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/pull_request_template.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000..dd84ea782 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,38 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - OS: [e.g. iOS] + - Browser [e.g. chrome, safari] + - Version [e.g. 22] + +**Smartphone (please complete the following information):** + - Device: [e.g. iPhone6] + - OS: [e.g. iOS8.1] + - Browser [e.g. stock browser, safari] + - Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 000000000..bbcbbe7d6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000..901fbe66f --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,20 @@ + +## Overview + +Brief description of what this PR does, and why it is needed. + +### Demo + +Optional. Screenshots, `curl` examples, etc. + +### Notes + +Optional. Ancillary topics, caveats, alternative strategies that didn't work out, anything else. + +## Testing Instructions + +* How to test this PR +* Prefer bulleted description +* Start after checking out this branch +* Include any setup required, such as bundling scripts, restarting services, etc. +* Include test case, and expected output \ No newline at end of file From c0317db4989dceb6fea37235d7ff1c6e75ed11e1 Mon Sep 17 00:00:00 2001 From: Jerry He Date: Sun, 11 Jul 2021 19:27:59 -0600 Subject: [PATCH 09/28] Added .yml file for CI/CD pipeline on interns' fork --- pipelines/interns-validatePR.yml | 59 ++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 pipelines/interns-validatePR.yml diff --git a/pipelines/interns-validatePR.yml b/pipelines/interns-validatePR.yml new file mode 100644 index 000000000..c6a4e7f21 --- /dev/null +++ b/pipelines/interns-validatePR.yml @@ -0,0 +1,59 @@ +# ASP.NET Core (.NET Framework) +# Build and test ASP.NET Core projects targeting the full .NET Framework. +# Add steps that publish symbols, save build artifacts, and more: +# https://docs.microsoft.com/azure/devops/pipelines/languages/dotnet-core + +trigger: + branches: + include: + - feature/* + - task/* + - fix/* + - interns/* + +pr: + branches: + include: + - feature/* + - task/* + - fix/* + - interns/* + +pool: + vmImage: 'windows-latest' + +variables: + solution: '**/*.sln' + buildPlatform: 'Any CPU' + buildConfiguration: 'Release' + +steps: +- checkout: self + clean: true + fetchDepth: 1 + submodules: true + +- task: NuGetToolInstaller@1 + +- task: NuGetCommand@2 + inputs: + restoreSolution: '$(solution)' + +- task: VSBuild@1 + inputs: + solution: '$(solution)' + msbuildArgs: '/p:WebPublishMethod=Package /p:PackageAsSingleFile=true /p:SkipInvalidConfigurations=true /p:DesktopBuildPackageLocation="$(build.artifactStagingDirectory)\WebApp.zip" /p:DeployIisAppPath="Default Web Site"' + platform: '$(buildPlatform)' + configuration: '$(buildConfiguration)' + +- task: VSTest@2 + inputs: + platform: '$(buildPlatform)' + configuration: '$(buildConfiguration)' + testAssemblyVer2: | + **/net5.0/**.Test.dll + !**/*TestAdapter.dll + !**/obj/** + diagnosticsEnabled: true + +- task: PublishBuildArtifacts@1 From ac2f1eec20c2df860f43e11c95e10f0a4abfca6f Mon Sep 17 00:00:00 2001 From: jerryxihe <46834951+jerryxihe@users.noreply.github.com> Date: Mon, 12 Jul 2021 16:10:58 -0600 Subject: [PATCH 10/28] Set up CI with Azure Pipelines [skip ci] --- pipelines/validatePR.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/pipelines/validatePR.yml b/pipelines/validatePR.yml index d59bd4d22..8bdd544af 100644 --- a/pipelines/validatePR.yml +++ b/pipelines/validatePR.yml @@ -10,6 +10,7 @@ pr: include: - dev - master + - interns/* pool: vmImage: 'windows-latest' From ba95e6cd221dc599a6b3df65aee9b5c395528eee Mon Sep 17 00:00:00 2001 From: Jerry He Date: Mon, 12 Jul 2021 19:28:44 -0600 Subject: [PATCH 11/28] Renamed interns .yml --- ...validatePR.yml => interns-build-and-deploy.yml} | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) rename pipelines/{interns-validatePR.yml => interns-build-and-deploy.yml} (71%) diff --git a/pipelines/interns-validatePR.yml b/pipelines/interns-build-and-deploy.yml similarity index 71% rename from pipelines/interns-validatePR.yml rename to pipelines/interns-build-and-deploy.yml index c6a4e7f21..680149016 100644 --- a/pipelines/interns-validatePR.yml +++ b/pipelines/interns-build-and-deploy.yml @@ -1,3 +1,5 @@ +# NOTE: This file is used by the Garage Interns for our Azure DevOps build and release pipelines. + # ASP.NET Core (.NET Framework) # Build and test ASP.NET Core projects targeting the full .NET Framework. # Add steps that publish symbols, save build artifacts, and more: @@ -31,8 +33,7 @@ steps: - checkout: self clean: true fetchDepth: 1 - submodules: true - + - task: NuGetToolInstaller@1 - task: NuGetCommand@2 @@ -56,4 +57,11 @@ steps: !**/obj/** diagnosticsEnabled: true -- task: PublishBuildArtifacts@1 +- task: VSBuild@1 + inputs: + solution: '$(solution)' + msbuildArgs: '/p:DeployOnBuild=true /p:WebPublishMethod=Package /p:PackageAsSingleFile=true /p:SkipInvalidConfigurations=true /p:DesktopBuildPackageLocation="$(build.artifactStagingDirectory)\WebApp.zip" /p:DeployIisAppPath="Default Web Site"' + platform: '$(buildPlatform)' + configuration: '$(buildConfiguration)' + +- task: PublishBuildArtifacts@1 \ No newline at end of file From 58023a997514eddce65d0b097491f2241a2b3859 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 13 Jul 2021 20:04:11 +0000 Subject: [PATCH 12/28] Bump Microsoft.AspNetCore.Authorization from 5.0.7 to 5.0.8 Bumps [Microsoft.AspNetCore.Authorization](https://github.com/dotnet/aspnetcore) from 5.0.7 to 5.0.8. - [Release notes](https://github.com/dotnet/aspnetcore/releases) - [Commits](https://github.com/dotnet/aspnetcore/compare/v5.0.7...v5.0.8) --- updated-dependencies: - dependency-name: Microsoft.AspNetCore.Authorization dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- GraphWebApi/GraphWebApi.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GraphWebApi/GraphWebApi.csproj b/GraphWebApi/GraphWebApi.csproj index 9daa4263b..4cacc1df5 100644 --- a/GraphWebApi/GraphWebApi.csproj +++ b/GraphWebApi/GraphWebApi.csproj @@ -46,7 +46,7 @@ - + From 3022c383e641e2d94e0e841c9b1aa24c9363370b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 13 Jul 2021 20:16:56 +0000 Subject: [PATCH 13/28] Bump Microsoft.AspNetCore.Mvc.NewtonsoftJson from 5.0.7 to 5.0.8 Bumps [Microsoft.AspNetCore.Mvc.NewtonsoftJson](https://github.com/dotnet/aspnetcore) from 5.0.7 to 5.0.8. - [Release notes](https://github.com/dotnet/aspnetcore/releases) - [Commits](https://github.com/dotnet/aspnetcore/compare/v5.0.7...v5.0.8) --- updated-dependencies: - dependency-name: Microsoft.AspNetCore.Mvc.NewtonsoftJson dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- GraphWebApi/GraphWebApi.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GraphWebApi/GraphWebApi.csproj b/GraphWebApi/GraphWebApi.csproj index 4cacc1df5..75726c1bc 100644 --- a/GraphWebApi/GraphWebApi.csproj +++ b/GraphWebApi/GraphWebApi.csproj @@ -47,7 +47,7 @@ - + From e8a133c9c73cc9f5da179f61c80f3d2ae3dcc803 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 13 Jul 2021 20:27:15 +0000 Subject: [PATCH 14/28] Bump Microsoft.AspNetCore.Authentication.JwtBearer from 5.0.7 to 5.0.8 Bumps [Microsoft.AspNetCore.Authentication.JwtBearer](https://github.com/dotnet/aspnetcore) from 5.0.7 to 5.0.8. - [Release notes](https://github.com/dotnet/aspnetcore/releases) - [Commits](https://github.com/dotnet/aspnetcore/compare/v5.0.7...v5.0.8) --- updated-dependencies: - dependency-name: Microsoft.AspNetCore.Authentication.JwtBearer dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- GraphWebApi/GraphWebApi.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GraphWebApi/GraphWebApi.csproj b/GraphWebApi/GraphWebApi.csproj index 75726c1bc..d2aab9936 100644 --- a/GraphWebApi/GraphWebApi.csproj +++ b/GraphWebApi/GraphWebApi.csproj @@ -45,7 +45,7 @@ - + From b245bf73dc44697abf16170b5bc42e436d93bbc8 Mon Sep 17 00:00:00 2001 From: jerryxihe <46834951+jerryxihe@users.noreply.github.com> Date: Tue, 13 Jul 2021 14:41:51 -0600 Subject: [PATCH 15/28] Revert "Set up CI with Azure Pipelines" --- pipelines/validatePR.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/pipelines/validatePR.yml b/pipelines/validatePR.yml index 8bdd544af..d59bd4d22 100644 --- a/pipelines/validatePR.yml +++ b/pipelines/validatePR.yml @@ -10,7 +10,6 @@ pr: include: - dev - master - - interns/* pool: vmImage: 'windows-latest' From 959a7b538ac30389ab16e6251ab5b43c49b44cbb Mon Sep 17 00:00:00 2001 From: Irvine Sunday <40403681+irvinesunday@users.noreply.github.com> Date: Tue, 13 Jul 2021 23:48:25 +0300 Subject: [PATCH 16/28] Fix: Get all distinct permissions (#636) * Refactor permissions fetching and update fetch-all permissions algo. * Add null checks; reorder params * Spacing * Variable renaming; add XML comment * Update permission tests Capture multiple scopes for a given endpoint/method to test whether distinct scopes gets returned * Add null checks; get distinct scopes; code refactoring * Update telemetry message * Update GraphExplorerPermissionsService/Services/PermissionsStore.cs Co-authored-by: Vincent Biret * PR review suggestions fixes * Update GraphExplorerPermissionsService/Services/PermissionsStore.cs Co-authored-by: Vincent Biret * Update GraphExplorerPermissionsService/Services/PermissionsStore.cs Co-authored-by: Vincent Biret * PR review suggestion fixes * Further PR review suggestion fixes - LINQ Select Co-authored-by: Irvine Sunday Co-authored-by: Vincent Biret --- .../Services/PermissionsStore.cs | 164 ++++++++---------- .../PermissionsStoreShould.cs | 9 +- .../permissions-test-file-v1.0_ver1.json | 2 + 3 files changed, 80 insertions(+), 95 deletions(-) diff --git a/GraphExplorerPermissionsService/Services/PermissionsStore.cs b/GraphExplorerPermissionsService/Services/PermissionsStore.cs index 17528b961..6cbe2a867 100644 --- a/GraphExplorerPermissionsService/Services/PermissionsStore.cs +++ b/GraphExplorerPermissionsService/Services/PermissionsStore.cs @@ -267,33 +267,22 @@ private IDictionary> CreateScopesI SeverityLevel.Information, _permissionsTraceProperties); - ScopesInformationList scopesInformationList = JsonConvert.DeserializeObject(scopesInfoJson); - - var _delegatedScopesInfoTable = new Dictionary(); - var _applicationScopesInfoTable = new Dictionary(); - - foreach (ScopeInformation delegatedScopeInfo in scopesInformationList.DelegatedScopesList) - { - _delegatedScopesInfoTable.Add(delegatedScopeInfo.ScopeName, delegatedScopeInfo); - } - - foreach (ScopeInformation applicationScopeInfo in scopesInformationList.ApplicationScopesList) - { - _applicationScopesInfoTable.Add(applicationScopeInfo.ScopeName, applicationScopeInfo); - } + var scopesInformationList = JsonConvert.DeserializeObject(scopesInfoJson); + var delegatedScopesInfoTable = scopesInformationList.DelegatedScopesList.ToDictionary(x => x.ScopeName); + var applicationScopesInfoTable = scopesInformationList.ApplicationScopesList.ToDictionary(x => x.ScopeName); _permissionsTraceProperties.Add(UtilityConstants.TelemetryPropertyKey_SanitizeIgnore, nameof(PermissionsStore)); _telemetryClient?.TrackTrace("Finished creating the scopes information tables. " + - $"Delegated scopes count: {_delegatedScopesInfoTable.Count}. " + - $"Application scopes count: {_applicationScopesInfoTable.Count}", + $"Delegated scopes count: {delegatedScopesInfoTable.Count}. " + + $"Application scopes count: {applicationScopesInfoTable.Count}", SeverityLevel.Information, _permissionsTraceProperties); _permissionsTraceProperties.Remove(UtilityConstants.TelemetryPropertyKey_SanitizeIgnore); return new Dictionary> { - { Delegated, _delegatedScopesInfoTable }, - { Application, _applicationScopesInfoTable } + { Delegated, delegatedScopesInfoTable }, + { Application, applicationScopesInfoTable } }; } @@ -341,40 +330,26 @@ public async Task> GetScopesAsync(string scopeType = "Del scopesInformationDictionary = await GetOrCreatePermissionsDescriptionsAsync(locale); } + var scopesInfo = new List(); + if (string.IsNullOrEmpty(requestUrl)) // fetch all permissions { _telemetryClient?.TrackTrace("Fetching all permissions", SeverityLevel.Information, _permissionsTraceProperties); - List scopesListInfo = new List(); + var scopes = _scopesListTable.Values.OfType() + .SelectMany(x => x).OfType() + .SelectMany(x => x.Value).OfType() + .Where(x => x.Name.Equals(scopeType)) + .SelectMany(x => x.Value) + .Values().Distinct().ToList(); - if (scopeType.Contains(Delegated)) - { - if (scopesInformationDictionary.ContainsKey(Delegated)) - { - foreach (var scopesInfo in scopesInformationDictionary[Delegated]) - { - scopesListInfo.Add(scopesInfo.Value); - } - } - } - else // Application scopes - { - if (scopesInformationDictionary.ContainsKey(Application)) - { - foreach (var scopesInfo in scopesInformationDictionary[Application]) - { - scopesListInfo.Add(scopesInfo.Value); - } - } - } + scopesInfo = GetScopesInformation(scopesInformationDictionary, scopes, scopeType); _telemetryClient?.TrackTrace("Return all permissions", SeverityLevel.Information, _permissionsTraceProperties); - - return scopesListInfo; } else // fetch permissions for a given request url and method { @@ -388,81 +363,52 @@ public async Task> GetScopesAsync(string scopeType = "Del // Check if requestUrl is contained in our Url Template table TemplateMatch resultMatch = _urlTemplateMatcher.Match(new Uri(requestUrl, UriKind.RelativeOrAbsolute)); - if (resultMatch == null) + if (resultMatch is null) { _telemetryClient?.TrackTrace($"Url '{requestUrl}' not found", - SeverityLevel.Error, - _permissionsTraceProperties); + SeverityLevel.Error, + _permissionsTraceProperties); return null; } - string[] scopes = null; - var resultValue = _scopesListTable[int.Parse(resultMatch.Key)] as JToken; - foreach (JProperty httpVerb in resultValue) + if (int.TryParse(resultMatch.Key, out int key) is false) { - if (httpVerb.Name.Equals(method, StringComparison.OrdinalIgnoreCase)) - { - foreach (JProperty scopeCategory in httpVerb.Value) - { - if (scopeCategory.Name.Equals(scopeType, StringComparison.OrdinalIgnoreCase)) - { - scopes = scopeCategory.Value?.ToObject(); - break; - } - } - } + throw new InvalidOperationException($"Failed to parse '{resultMatch.Key}' to int."); } - if (scopes == null) + if (_scopesListTable[key] is not JToken resultValue) { - _telemetryClient?.TrackTrace($"No '{scopeType}' permissions found for the url '{requestUrl}' and method '{method}'", + _telemetryClient?.TrackTrace($"Key '{_scopesListTable[key]}' in the {nameof(_scopesListTable)} has a null value.", SeverityLevel.Error, _permissionsTraceProperties); return null; } - List scopesList = new List(); + var scopes = resultValue.OfType() + .Where(x => method.Equals(x.Name, StringComparison.OrdinalIgnoreCase)) + .SelectMany(x => x.Value).OfType() + .FirstOrDefault(x => scopeType.Equals(x.Name, StringComparison.OrdinalIgnoreCase)) + ?.Value?.ToObject>().Distinct().ToList(); - foreach (string scopeName in scopes) + if (scopes is null) { - ScopeInformation scopeInfo = null; + _telemetryClient?.TrackTrace($"No '{scopeType}' permissions found for the url '{requestUrl}' and method '{method}'", + SeverityLevel.Error, + _permissionsTraceProperties); - if (scopeType.Contains(Delegated)) - { - if (scopesInformationDictionary[Delegated].ContainsKey(scopeName)) - { - scopeInfo = scopesInformationDictionary[Delegated][scopeName]; - } - } - else // Application scopes - { - if (scopesInformationDictionary[Application].ContainsKey(scopeName)) - { - scopeInfo = scopesInformationDictionary[Application][scopeName]; - } - } - if (scopeInfo == null) - { - scopesList.Add(new ScopeInformation - { - ScopeName = scopeName - }); - } - else - { - scopesList.Add(scopeInfo); - } + return null; } + scopesInfo = GetScopesInformation(scopesInformationDictionary, scopes, scopeType); + _telemetryClient?.TrackTrace($"Return '{scopeType}' permissions for url '{requestUrl}' and method '{method}'", SeverityLevel.Information, _permissionsTraceProperties); - - return scopesList; - } + + return scopesInfo; } /// @@ -529,6 +475,40 @@ private static string CleanRequestUrl(string requestUrl) .ToLowerInvariant(); } + /// + /// Retrieves the scopes information for a given list of scopes. + /// + /// The source of the scopes information. + /// The target list of scopes. + /// The type of scope from which to retrieve the scopes information for. + /// A list of . + private static List GetScopesInformation(IDictionary> scopesInformationDictionary, + List scopes, + string scopeType) + { + if (scopesInformationDictionary is null) + { + throw new ArgumentNullException(nameof(scopesInformationDictionary)); + } + + if (scopes is null) + { + throw new ArgumentNullException(nameof(scopes)); + } + + if (string.IsNullOrEmpty(scopeType)) + { + throw new ArgumentException($"'{nameof(scopeType)}' cannot be null or empty.", nameof(scopeType)); + } + + var key = scopeType.Contains(Delegated) ? Delegated : Application; + return scopes.Select(scope => + { + scopesInformationDictionary[key].TryGetValue(scope, out var scopeInfo); + return scopeInfo ?? new() { ScopeName = scope }; + }).ToList(); + } + /// public UriTemplateMatcher GetUriTemplateMatcher() { @@ -536,4 +516,4 @@ public UriTemplateMatcher GetUriTemplateMatcher() return _urlTemplateMatcher; } } -} \ No newline at end of file +} diff --git a/PermissionsService.Test/PermissionsStoreShould.cs b/PermissionsService.Test/PermissionsStoreShould.cs index 63824f5f0..b6d49f583 100644 --- a/PermissionsService.Test/PermissionsStoreShould.cs +++ b/PermissionsService.Test/PermissionsStoreShould.cs @@ -46,14 +46,17 @@ public void GetRequiredPermissionScopesGivenAnExistingRequestUrl() }); } - [Fact] - public void GetAllPermissionScopesGivenNoRequestUrl() + [Theory] + [InlineData("DelegatedWork", 20)] + [InlineData("Application", 14)] + public void GetAllPermissionScopesGivenNoRequestUrl(string scopeType, int expectedCount) { // Act - List result = _permissionsStore.GetScopesAsync().GetAwaiter().GetResult(); + List result = _permissionsStore.GetScopesAsync(scopeType: scopeType).GetAwaiter().GetResult(); // Assert Assert.NotEmpty(result); + Assert.Equal(expectedCount, result.Count); } [Fact] diff --git a/PermissionsService.Test/TestFiles/permissions-test-file-v1.0_ver1.json b/PermissionsService.Test/TestFiles/permissions-test-file-v1.0_ver1.json index 6a1d236a7..e185b55e4 100644 --- a/PermissionsService.Test/TestFiles/permissions-test-file-v1.0_ver1.json +++ b/PermissionsService.Test/TestFiles/permissions-test-file-v1.0_ver1.json @@ -93,6 +93,8 @@ "GET": { "DelegatedWork": [ + "Calendars.Read", + "Calendars.Read", "Calendars.Read" ], "DelegatedPersonal": [ From e4935cac9ea54ba04449d35bcb23956ebd0eb92f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 13 Jul 2021 20:56:14 +0000 Subject: [PATCH 17/28] Bump Microsoft.Extensions.DependencyInjection from 5.0.1 to 5.0.2 Bumps [Microsoft.Extensions.DependencyInjection](https://github.com/dotnet/runtime) from 5.0.1 to 5.0.2. - [Release notes](https://github.com/dotnet/runtime/releases) - [Commits](https://github.com/dotnet/runtime/compare/v5.0.1...v5.0.2) --- updated-dependencies: - dependency-name: Microsoft.Extensions.DependencyInjection dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- TelemetryService.Test/TelemetrySanitizerService.Test.csproj | 2 +- TelemetryService/TelemetrySanitizerService.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/TelemetryService.Test/TelemetrySanitizerService.Test.csproj b/TelemetryService.Test/TelemetrySanitizerService.Test.csproj index 227459787..a9fdd1899 100644 --- a/TelemetryService.Test/TelemetrySanitizerService.Test.csproj +++ b/TelemetryService.Test/TelemetrySanitizerService.Test.csproj @@ -20,7 +20,7 @@ - + diff --git a/TelemetryService/TelemetrySanitizerService.csproj b/TelemetryService/TelemetrySanitizerService.csproj index 5ecc6d0d1..6cc6d8d9d 100644 --- a/TelemetryService/TelemetrySanitizerService.csproj +++ b/TelemetryService/TelemetrySanitizerService.csproj @@ -6,7 +6,7 @@ - + From 16444e24823e93ba2096f4647b10f3cf104f4863 Mon Sep 17 00:00:00 2001 From: Jerry He Date: Thu, 15 Jul 2021 11:09:38 -0600 Subject: [PATCH 18/28] Renamed interns pipeline .yml to accurately reflect its use --- ...interns-build-and-deploy.yml => interns-build-and-publish.yml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename pipelines/{interns-build-and-deploy.yml => interns-build-and-publish.yml} (100%) diff --git a/pipelines/interns-build-and-deploy.yml b/pipelines/interns-build-and-publish.yml similarity index 100% rename from pipelines/interns-build-and-deploy.yml rename to pipelines/interns-build-and-publish.yml From 4b402c686bca1b057f9892cd09cf6ad33cd857f7 Mon Sep 17 00:00:00 2001 From: jerryxihe <46834951+jerryxihe@users.noreply.github.com> Date: Thu, 15 Jul 2021 11:48:47 -0600 Subject: [PATCH 19/28] AB#38984 Merging interns pipeline .yml into LokiLabs/dev (#8) * Added .yml file for CI/CD pipeline on interns' fork * Set up CI with Azure Pipelines [skip ci] * Revert "Set up CI with Azure Pipelines" * Renamed interns .yml * Renamed interns pipeline .yml to accurately reflect its use Co-authored-by: allison <47487758+acchiang@users.noreply.github.com> --- pipelines/interns-build-and-publish.yml | 67 +++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 pipelines/interns-build-and-publish.yml diff --git a/pipelines/interns-build-and-publish.yml b/pipelines/interns-build-and-publish.yml new file mode 100644 index 000000000..680149016 --- /dev/null +++ b/pipelines/interns-build-and-publish.yml @@ -0,0 +1,67 @@ +# NOTE: This file is used by the Garage Interns for our Azure DevOps build and release pipelines. + +# ASP.NET Core (.NET Framework) +# Build and test ASP.NET Core projects targeting the full .NET Framework. +# Add steps that publish symbols, save build artifacts, and more: +# https://docs.microsoft.com/azure/devops/pipelines/languages/dotnet-core + +trigger: + branches: + include: + - feature/* + - task/* + - fix/* + - interns/* + +pr: + branches: + include: + - feature/* + - task/* + - fix/* + - interns/* + +pool: + vmImage: 'windows-latest' + +variables: + solution: '**/*.sln' + buildPlatform: 'Any CPU' + buildConfiguration: 'Release' + +steps: +- checkout: self + clean: true + fetchDepth: 1 + +- task: NuGetToolInstaller@1 + +- task: NuGetCommand@2 + inputs: + restoreSolution: '$(solution)' + +- task: VSBuild@1 + inputs: + solution: '$(solution)' + msbuildArgs: '/p:WebPublishMethod=Package /p:PackageAsSingleFile=true /p:SkipInvalidConfigurations=true /p:DesktopBuildPackageLocation="$(build.artifactStagingDirectory)\WebApp.zip" /p:DeployIisAppPath="Default Web Site"' + platform: '$(buildPlatform)' + configuration: '$(buildConfiguration)' + +- task: VSTest@2 + inputs: + platform: '$(buildPlatform)' + configuration: '$(buildConfiguration)' + testAssemblyVer2: | + **/net5.0/**.Test.dll + !**/*TestAdapter.dll + !**/obj/** + diagnosticsEnabled: true + +- task: VSBuild@1 + inputs: + solution: '$(solution)' + msbuildArgs: '/p:DeployOnBuild=true /p:WebPublishMethod=Package /p:PackageAsSingleFile=true /p:SkipInvalidConfigurations=true /p:DesktopBuildPackageLocation="$(build.artifactStagingDirectory)\WebApp.zip" /p:DeployIisAppPath="Default Web Site"' + platform: '$(buildPlatform)' + configuration: '$(buildConfiguration)' + +- task: PublishBuildArtifacts@1 \ No newline at end of file From 151fc603e378d630b7882eb6b07a27d9e9260718 Mon Sep 17 00:00:00 2001 From: Jerry He Date: Thu, 15 Jul 2021 12:08:09 -0600 Subject: [PATCH 20/28] Added `master` and `dev` to interns pipeline triggers --- pipelines/interns-build-and-publish.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pipelines/interns-build-and-publish.yml b/pipelines/interns-build-and-publish.yml index 680149016..a735397cf 100644 --- a/pipelines/interns-build-and-publish.yml +++ b/pipelines/interns-build-and-publish.yml @@ -8,6 +8,8 @@ trigger: branches: include: + - master + - dev - feature/* - task/* - fix/* @@ -16,6 +18,8 @@ trigger: pr: branches: include: + - master + - dev - feature/* - task/* - fix/* From 5f0f758689542df4071dc1f515c4e91acf39d878 Mon Sep 17 00:00:00 2001 From: Omer Abubaker Date: Tue, 20 Jul 2021 16:06:55 -0400 Subject: [PATCH 21/28] Added teamsAppSampleQueries in model and extended ordering functionality --- GraphExplorerSamplesService/Models/SampleQueriesList.cs | 2 ++ GraphExplorerSamplesService/Services/SamplesService.cs | 7 +++++++ 2 files changed, 9 insertions(+) diff --git a/GraphExplorerSamplesService/Models/SampleQueriesList.cs b/GraphExplorerSamplesService/Models/SampleQueriesList.cs index b5a668373..18c707868 100644 --- a/GraphExplorerSamplesService/Models/SampleQueriesList.cs +++ b/GraphExplorerSamplesService/Models/SampleQueriesList.cs @@ -11,10 +11,12 @@ public class SampleQueriesList /// A list of objects /// public List SampleQueries { get; set; } + public List TeamsAppSampleQueries { get; set; } public SampleQueriesList() { SampleQueries = new List(); + TeamsAppSampleQueries = new List(); } } } diff --git a/GraphExplorerSamplesService/Services/SamplesService.cs b/GraphExplorerSamplesService/Services/SamplesService.cs index 959aef8ef..b670129a5 100644 --- a/GraphExplorerSamplesService/Services/SamplesService.cs +++ b/GraphExplorerSamplesService/Services/SamplesService.cs @@ -211,13 +211,20 @@ private static SampleQueriesList OrderSamplesQueries(SampleQueriesList sampleQue .Where(s => s.Category != "Getting Started") // skipped, as it should always be the top-most sample query in the list .ToList(); + List teamsAppSortedSampleQueries = sampleQueriesList.TeamsAppSampleQueries + .OrderBy(s => s.Category) + .Where(s => s.Category != "Getting Started") // skipped, as it should always be the top-most sample query in the list + .ToList(); + SampleQueriesList sortedSampleQueriesList = new SampleQueriesList(); // Add back 'Getting Started' to the top of the list sortedSampleQueriesList.SampleQueries.AddRange(sampleQueriesList.SampleQueries.FindAll(s => s.Category == "Getting Started")); + sortedSampleQueriesList.TeamsAppSampleQueries.AddRange(sampleQueriesList.SampleQueries.FindAll(s => s.Category == "Getting Started")); // Add the rest of the sample queries sortedSampleQueriesList.SampleQueries.AddRange(sortedSampleQueries); + sortedSampleQueriesList.SampleQueries.AddRange(teamsAppSortedSampleQueries); return sortedSampleQueriesList; } From d60609a825766c28e513cc878a9555b5399eca3d Mon Sep 17 00:00:00 2001 From: Omer Abubaker Date: Tue, 20 Jul 2021 16:30:38 -0400 Subject: [PATCH 22/28] Fixed small error --- GraphExplorerSamplesService/Services/SamplesService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GraphExplorerSamplesService/Services/SamplesService.cs b/GraphExplorerSamplesService/Services/SamplesService.cs index b670129a5..d767fe46d 100644 --- a/GraphExplorerSamplesService/Services/SamplesService.cs +++ b/GraphExplorerSamplesService/Services/SamplesService.cs @@ -220,7 +220,7 @@ private static SampleQueriesList OrderSamplesQueries(SampleQueriesList sampleQue // Add back 'Getting Started' to the top of the list sortedSampleQueriesList.SampleQueries.AddRange(sampleQueriesList.SampleQueries.FindAll(s => s.Category == "Getting Started")); - sortedSampleQueriesList.TeamsAppSampleQueries.AddRange(sampleQueriesList.SampleQueries.FindAll(s => s.Category == "Getting Started")); + sortedSampleQueriesList.TeamsAppSampleQueries.AddRange(sampleQueriesList.TeamsAppSampleQueries.FindAll(s => s.Category == "Getting Started")); // Add the rest of the sample queries sortedSampleQueriesList.SampleQueries.AddRange(sortedSampleQueries); From 44611b2ac432ac3eaffc81aa48a806fb7b872f0e Mon Sep 17 00:00:00 2001 From: Omer Abubaker Date: Wed, 21 Jul 2021 17:03:39 -0400 Subject: [PATCH 23/28] Fixed another small typo --- GraphExplorerSamplesService/Services/SamplesService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GraphExplorerSamplesService/Services/SamplesService.cs b/GraphExplorerSamplesService/Services/SamplesService.cs index d767fe46d..d8965b96a 100644 --- a/GraphExplorerSamplesService/Services/SamplesService.cs +++ b/GraphExplorerSamplesService/Services/SamplesService.cs @@ -224,7 +224,7 @@ private static SampleQueriesList OrderSamplesQueries(SampleQueriesList sampleQue // Add the rest of the sample queries sortedSampleQueriesList.SampleQueries.AddRange(sortedSampleQueries); - sortedSampleQueriesList.SampleQueries.AddRange(teamsAppSortedSampleQueries); + sortedSampleQueriesList.TeamsAppSampleQueries.AddRange(teamsAppSortedSampleQueries); return sortedSampleQueriesList; } From a924fc0eec33b0c94222d0a335b7a6a3113cb470 Mon Sep 17 00:00:00 2001 From: Omer Abubaker Date: Mon, 26 Jul 2021 21:46:08 -0400 Subject: [PATCH 24/28] Added ownership check functions --- .../Interfaces/IGraphService.cs | 15 ++ .../Interfaces/IGraphServiceClient.cs | 12 -- .../Services/GraphAppAuthProvider.cs | 8 - .../Services/GraphClientService.cs | 18 -- .../Services/GraphService.cs | 84 +++++++++ .../GraphExplorerAppModeController.cs | 177 +++++++++++------- GraphWebApi/Startup.cs | 1 + 7 files changed, 209 insertions(+), 106 deletions(-) create mode 100644 GraphExplorerAppModeService/Interfaces/IGraphService.cs delete mode 100644 GraphExplorerAppModeService/Interfaces/IGraphServiceClient.cs delete mode 100644 GraphExplorerAppModeService/Services/GraphClientService.cs create mode 100644 GraphExplorerAppModeService/Services/GraphService.cs diff --git a/GraphExplorerAppModeService/Interfaces/IGraphService.cs b/GraphExplorerAppModeService/Interfaces/IGraphService.cs new file mode 100644 index 000000000..966bd4ca1 --- /dev/null +++ b/GraphExplorerAppModeService/Interfaces/IGraphService.cs @@ -0,0 +1,15 @@ +using Microsoft.Graph; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace GraphExplorerAppModeService.Interfaces +{ + public interface IGraphService + { + string ErrorMessage { get; set; } + Task VerifyOwnership(GraphServiceClient graphClient, string query, string clientId); + } +} diff --git a/GraphExplorerAppModeService/Interfaces/IGraphServiceClient.cs b/GraphExplorerAppModeService/Interfaces/IGraphServiceClient.cs deleted file mode 100644 index 21d8c5757..000000000 --- a/GraphExplorerAppModeService/Interfaces/IGraphServiceClient.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace GraphExplorerAppModeService.Interfaces -{ - interface IGraphServiceClient - { - } -} diff --git a/GraphExplorerAppModeService/Services/GraphAppAuthProvider.cs b/GraphExplorerAppModeService/Services/GraphAppAuthProvider.cs index 877ebea9c..952cd8589 100644 --- a/GraphExplorerAppModeService/Services/GraphAppAuthProvider.cs +++ b/GraphExplorerAppModeService/Services/GraphAppAuthProvider.cs @@ -1,14 +1,6 @@ using GraphExplorerAppModeService.Interfaces; -using Microsoft.Extensions.Configuration; using Microsoft.Graph; -using Microsoft.Identity.Web; -using System; -using System.Collections.Generic; -using System.Linq; using System.Net.Http.Headers; -using System.Text; -using System.Threading.Tasks; - namespace GraphExplorerAppModeService.Services { diff --git a/GraphExplorerAppModeService/Services/GraphClientService.cs b/GraphExplorerAppModeService/Services/GraphClientService.cs deleted file mode 100644 index eb90cd15b..000000000 --- a/GraphExplorerAppModeService/Services/GraphClientService.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using GraphExplorerAppModeService.Interfaces; -using Microsoft.Extensions.Configuration; - -namespace GraphExplorerAppModeService.Services -{ - class GraphClientService : IGraphServiceClient - { - public GraphClientService(IConfiguration configuration) - { - - } - } -} diff --git a/GraphExplorerAppModeService/Services/GraphService.cs b/GraphExplorerAppModeService/Services/GraphService.cs new file mode 100644 index 000000000..c66f10e8a --- /dev/null +++ b/GraphExplorerAppModeService/Services/GraphService.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using GraphExplorerAppModeService.Interfaces; +using Microsoft.Extensions.Configuration; +using Microsoft.Graph; +using Newtonsoft.Json; + +namespace GraphExplorerAppModeService.Services +{ + public class GraphService : IGraphService + { + private string errorMessage; + + /// + /// ErrorMessage will contain the error message from the Graph Client call. + /// + public string ErrorMessage + { + get { return errorMessage; } + set { errorMessage = value; } + } + + public async Task VerifyOwnership(GraphServiceClient graphClient, string query, string clientId) + { + string[] queryList = query.Split("/"); + + for (int i=0; i < queryList.Length; i++) + { + if (queryList[i] == "teams") + { + return await VerifyTeamsOwnership(graphClient, queryList[i + 1], clientId); + } else if (queryList[i] == "chats") + { + return await VerifyChatOwnership(graphClient, queryList[i + 1], clientId); + } + } + + return false; + } + + private async Task VerifyTeamsOwnership(GraphServiceClient graphClient, string teamId, string clientId) + { + try + { + var owners = await graphClient.Groups[teamId].Owners.Request().GetAsync(); + + foreach (var owner in owners) + { + if (clientId == owner.Id) return true; + } + } + catch (ServiceException e) + { + ErrorMessage = e.Message; + } + + return false; + } + + private async Task VerifyChatOwnership(GraphServiceClient graphClient, string chatId, string clientId) + { + try + { + var members = await graphClient.Chats[chatId].Members.Request().GetAsync(); + + foreach (AadUserConversationMember member in members) + { + if (clientId == member.UserId && member.Roles.Contains("owner")) return true; + Debug.WriteLine(member); + } + } + catch (ServiceException e) + { + ErrorMessage = e.Message; + } + + return false; + } + } +} diff --git a/GraphWebApi/Controllers/GraphExplorerAppModeController.cs b/GraphWebApi/Controllers/GraphExplorerAppModeController.cs index 4d311b3b9..85380a919 100644 --- a/GraphWebApi/Controllers/GraphExplorerAppModeController.cs +++ b/GraphWebApi/Controllers/GraphExplorerAppModeController.cs @@ -1,6 +1,5 @@ using System; using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using System.Collections.Generic; using System.Linq; @@ -10,54 +9,47 @@ using System.Net.Http.Headers; using Microsoft.Graph; using System.Net; -using Microsoft.Identity.Client; using System.Threading; using Microsoft.Extensions.Primitives; using GraphExplorerAppModeService.Services; using GraphExplorerAppModeService.Interfaces; using Microsoft.Extensions.Configuration; +using System.IdentityModel.Tokens.Jwt; namespace GraphWebApi.Controllers { [ApiController] public class GraphExplorerAppModeController : ControllerBase { - private readonly IGraphAppAuthProvider _graphClient; + private readonly IGraphAppAuthProvider _graphAuthClient; private readonly ITokenAcquisition _tokenAcquisition; - private readonly IConfiguration _config; + private readonly IGraphService _graphService; - public GraphExplorerAppModeController(IConfiguration configuration, ITokenAcquisition tokenAcquisition, IGraphAppAuthProvider graphServiceClient) + public GraphExplorerAppModeController(IConfiguration configuration, ITokenAcquisition tokenAcquisition, IGraphAppAuthProvider graphServiceClient, IGraphService graphService) { - this._graphClient = graphServiceClient; + this._graphAuthClient = graphServiceClient; this._tokenAcquisition = tokenAcquisition; this._config = configuration; + this._graphService = graphService; } + [Route("api/[controller]/{*all}")] [Route("graphproxy/{*all}")] [HttpGet] [AuthorizeForScopes(Scopes = new[] { "https://graph.microsoft.com/.default" })] - public async Task GetAsync(string all) + public async Task GetAsync(string all, [FromHeader] string Authorization) { - return await this.ProcessRequestAsync("GET", all, null, _config.GetSection("AzureAd").GetSection("TenantId").Value).ConfigureAwait(false); + return await this.ProcessRequestAsync("GET", all, null, Authorization).ConfigureAwait(false); } - private async Task GetTokenAsync(string tenantId) - { - // Acquire the access token. - string scopes = "https://graph.microsoft.com/.default"; - - return await _tokenAcquisition.GetAccessTokenForAppAsync(scopes, tenantId, null); - } - - [Route("api/[controller]/{*all}")] [Route("graphproxy/{*all}")] [HttpPost] [AuthorizeForScopes(Scopes = new[] { "https://graph.microsoft.com/.default" })] - public async Task PostAsync(string all, [FromBody] object body) + public async Task PostAsync(string all, [FromBody] object body, [FromHeader] string Authorization) { - return await ProcessRequestAsync("POST", all, body, _config.GetSection("AzureAd").GetSection("TenantId").Value).ConfigureAwait(false); + return await ProcessRequestAsync("POST", all, body, Authorization).ConfigureAwait(false); } [Route("api/[controller]/{*all}")] @@ -65,71 +57,76 @@ public async Task PostAsync(string all, [FromBody] object body) [HttpDelete] public async Task DeleteAsync(string all, [FromHeader] string Authorization) { - return await ProcessRequestAsync("DELETE", all, null, _config.GetSection("AzureAd").GetSection("TenantId").Value).ConfigureAwait(false); + return await ProcessRequestAsync("DELETE", all, null, Authorization).ConfigureAwait(false); } [Route("api/[controller]/{*all}")] [Route("graphproxy/{*all}")] [HttpPut] [AuthorizeForScopes(Scopes = new[] { "https://graph.microsoft.com/.default" })] - public async Task PutAsync(string all, [FromBody] object body) + public async Task PutAsync(string all, [FromBody] object body, [FromHeader] string Authorization) { - return await ProcessRequestAsync("PUT", all, body, _config.GetSection("AzureAd").GetSection("TenantId").Value).ConfigureAwait(false); + return await ProcessRequestAsync("PUT", all, body, Authorization).ConfigureAwait(false); } - private string GetBaseUrlWithoutVersion(GraphServiceClient graphClient) + [Route("api/[controller]/{*all}")] + [Route("graphproxy/{*all}")] + [HttpPatch] + [AuthorizeForScopes(Scopes = new[] { "https://graph.microsoft.com/.default" })] + public async Task PatchAsync(string all, [FromBody] object body, [FromHeader] string Authorization) { - var baseUrl = graphClient.BaseUrl; - var index = baseUrl.LastIndexOf('/'); - return baseUrl.Substring(0, index); + return await ProcessRequestAsync("PATCH", all, body, Authorization).ConfigureAwait(false); } - public class HttpResponseMessageResult : IActionResult + private async Task ProcessRequestAsync(string method, string all, object content, string Authorization) { - private readonly HttpResponseMessage _responseMessage; + // decode JWT Auth token + string userToken = Authorization.Split(" ")[1]; + JwtSecurityToken authToken = new JwtSecurityTokenHandler().ReadJwtToken(userToken); - public HttpResponseMessageResult(HttpResponseMessage responseMessage) - { - _responseMessage = responseMessage; // could add throw if null - } + string tenantId = "8d87ccfa-0037-44f0-ac71-31c71ac81a2b"; + string clientId = "8d87ccfa-0037-44f0-ac71-31c71ac81a2b"; - public async Task ExecuteResultAsync(ActionContext context) + string errorContentType = "application/json"; + try { - context.HttpContext.Response.StatusCode = (int)_responseMessage.StatusCode; + // Authentication provider using a generated application context token + GraphServiceClient graphServiceClient = _graphAuthClient.GetAuthenticatedGraphClient(GetTokenAsync(tenantId).Result.ToString()); - foreach (var header in _responseMessage.Headers) - { - context.HttpContext.Response.Headers.TryAdd(header.Key, new StringValues(header.Value.ToArray())); - } + // Processing the graph request + GraphResponse processedGraphRequest = await ProcessGraphRequest(method, all, content, graphServiceClient); + + // Authentication provider using user's delegated token + GraphServiceClient userGraphServiceClient = _graphAuthClient.GetAuthenticatedGraphClient(userToken); - context.HttpContext.Response.ContentType = _responseMessage.Content.Headers.ContentType.ToString(); + // Check if user owns the resource in question + bool userOwnership = await _graphService.VerifyOwnership(userGraphServiceClient, all, clientId); - using (var stream = await _responseMessage.Content.ReadAsStreamAsync()) + if (userOwnership) { + return new HttpResponseMessageResult(ReturnHttpResponseMessage(HttpStatusCode.OK, processedGraphRequest.contentType, new ByteArrayContent(processedGraphRequest.contentByteArray))); + } else { - await stream.CopyToAsync(context.HttpContext.Response.Body); - await context.HttpContext.Response.Body.FlushAsync(); + return new HttpResponseMessageResult(ReturnHttpResponseMessage(HttpStatusCode.Forbidden, errorContentType, new StringContent(""))); } } + catch (ServiceException ex) + { + return new HttpResponseMessageResult(ReturnHttpResponseMessage(ex.StatusCode, errorContentType, new StringContent(ex.Error.ToString()))); + } + } - [Route("api/[controller]/{*all}")] - [Route("graphproxy/{*all}")] - [HttpPatch] - [AuthorizeForScopes(Scopes = new[] { "https://graph.microsoft.com/.default" })] - public async Task PatchAsync(string all, [FromBody] object body) + struct GraphResponse { - return await ProcessRequestAsync("PATCH", all, body, _config.GetSection("AzureAd").GetSection("TenantId").Value).ConfigureAwait(false); + public string contentType; + public byte[] contentByteArray; } - private async Task ProcessRequestAsync(string method, string all, object content, string tenantId) + private async Task ProcessGraphRequest(string method, string all, object content, GraphServiceClient graphClient) { - GraphServiceClient _graphServiceClient = _graphClient.GetAuthenticatedGraphClient(GetTokenAsync(tenantId).Result.ToString()); + var url = $"{GetBaseUrlWithoutVersion(graphClient)}/{all}{HttpContext.Request.QueryString.ToUriComponent()}"; - var qs = HttpContext.Request.QueryString; - - var url = $"{GetBaseUrlWithoutVersion(_graphServiceClient)}/{all}{qs.ToUriComponent()}"; - - var request = new BaseRequest(url, _graphServiceClient, null) + var request = new BaseRequest(url, graphClient, null) { Method = method, ContentType = HttpContext.Request.ContentType, @@ -145,25 +142,32 @@ private async Task ProcessRequestAsync(string method, string all, } var contentType = "application/json"; - try + + using (var response = await request.SendRequestAsync(content?.ToString(), CancellationToken.None, HttpCompletionOption.ResponseContentRead).ConfigureAwait(false)) { - using (var response = await request.SendRequestAsync(content?.ToString(), CancellationToken.None, HttpCompletionOption.ResponseContentRead).ConfigureAwait(false)) - { - response.Content.Headers.TryGetValues("content-type", out var contentTypes); + response.Content.Headers.TryGetValues("content-type", out var contentTypes); - contentType = contentTypes?.FirstOrDefault() ?? contentType; + contentType = contentTypes?.FirstOrDefault() ?? contentType; - var byteArrayContent = await response.Content.ReadAsByteArrayAsync().ConfigureAwait(false); - Console.WriteLine(byteArrayContent); - return new HttpResponseMessageResult(ReturnHttpResponseMessage(HttpStatusCode.OK, contentType, new ByteArrayContent(byteArrayContent))); - } - } - catch (ServiceException ex) - { - return new HttpResponseMessageResult(ReturnHttpResponseMessage(ex.StatusCode, contentType, new StringContent(ex.Error.ToString()))); + var byteArrayContent = await response.Content.ReadAsByteArrayAsync().ConfigureAwait(false); + Console.WriteLine(byteArrayContent); + + return new GraphResponse + { + contentByteArray = byteArrayContent, + contentType = contentType + }; } } + // Acquire the application context access token. + private async Task GetTokenAsync(string tenantId) + { + string scopes = "https://graph.microsoft.com/.default"; + + return await _tokenAcquisition.GetAccessTokenForAppAsync(scopes, tenantId, null); + } + private static HttpResponseMessage ReturnHttpResponseMessage(HttpStatusCode httpStatusCode, string contentType, HttpContent httpContent) { var httpResponseMessage = new HttpResponseMessage(httpStatusCode) @@ -182,5 +186,42 @@ private static HttpResponseMessage ReturnHttpResponseMessage(HttpStatusCode http return httpResponseMessage; } + + private string GetBaseUrlWithoutVersion(GraphServiceClient graphClient) + { + var baseUrl = graphClient.BaseUrl; + var index = baseUrl.LastIndexOf('/'); + return baseUrl.Substring(0, index); + } + + public class HttpResponseMessageResult : IActionResult + { + private readonly HttpResponseMessage _responseMessage; + + public HttpResponseMessageResult(HttpResponseMessage responseMessage) + { + _responseMessage = responseMessage; // could add throw if null + } + + public async Task ExecuteResultAsync(ActionContext context) + { + context.HttpContext.Response.StatusCode = (int)_responseMessage.StatusCode; + + foreach (var header in _responseMessage.Headers) + { + context.HttpContext.Response.Headers.TryAdd(header.Key, new StringValues(header.Value.ToArray())); + } + + context.HttpContext.Response.ContentType = _responseMessage.Content.Headers.ContentType.ToString(); + + using (var stream = await _responseMessage.Content.ReadAsStreamAsync()) + { + await stream.CopyToAsync(context.HttpContext.Response.Body); + await context.HttpContext.Response.Body.FlushAsync(); + } + } + } + + } } diff --git a/GraphWebApi/Startup.cs b/GraphWebApi/Startup.cs index 342d1aab5..9ac807ecd 100644 --- a/GraphWebApi/Startup.cs +++ b/GraphWebApi/Startup.cs @@ -107,6 +107,7 @@ public void ConfigureServices(IServiceCollection services) services.AddMemoryCache(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); From 79c0986e7b602963eb82d4fea7c1fee899ad792c Mon Sep 17 00:00:00 2001 From: Omer Abubaker Date: Wed, 28 Jul 2021 16:41:00 -0400 Subject: [PATCH 25/28] Finalized tenant and client id retrieval from token --- .../Controllers/GraphExplorerAppModeController.cs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/GraphWebApi/Controllers/GraphExplorerAppModeController.cs b/GraphWebApi/Controllers/GraphExplorerAppModeController.cs index 85380a919..a59a30ce7 100644 --- a/GraphWebApi/Controllers/GraphExplorerAppModeController.cs +++ b/GraphWebApi/Controllers/GraphExplorerAppModeController.cs @@ -15,6 +15,9 @@ using GraphExplorerAppModeService.Interfaces; using Microsoft.Extensions.Configuration; using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using Microsoft.IdentityModel.Tokens; +using Microsoft.IdentityModel.Logging; namespace GraphWebApi.Controllers { @@ -82,10 +85,11 @@ private async Task ProcessRequestAsync(string method, string all, { // decode JWT Auth token string userToken = Authorization.Split(" ")[1]; - JwtSecurityToken authToken = new JwtSecurityTokenHandler().ReadJwtToken(userToken); - - string tenantId = "8d87ccfa-0037-44f0-ac71-31c71ac81a2b"; - string clientId = "8d87ccfa-0037-44f0-ac71-31c71ac81a2b"; + + // Retrieve tenantId and clientId from token + IEnumerable jwtTokenClaims = new JwtSecurityToken(userToken).Claims; + string tenantId = jwtTokenClaims.First(claim => claim.Type == "tid").Value; + string clientId = jwtTokenClaims.First(claim => claim.Type == "oid").Value; string errorContentType = "application/json"; try From 9d70e8f91df7a9ba1022a844c93190301fdf01dd Mon Sep 17 00:00:00 2001 From: Omer Abubaker Date: Wed, 28 Jul 2021 18:49:13 -0400 Subject: [PATCH 26/28] Cleanup --- GraphExplorerAppModeService/Services/GraphService.cs | 1 - GraphWebApi/Controllers/GraphExplorerAppModeController.cs | 6 +----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/GraphExplorerAppModeService/Services/GraphService.cs b/GraphExplorerAppModeService/Services/GraphService.cs index c66f10e8a..71bd927a0 100644 --- a/GraphExplorerAppModeService/Services/GraphService.cs +++ b/GraphExplorerAppModeService/Services/GraphService.cs @@ -70,7 +70,6 @@ private async Task VerifyChatOwnership(GraphServiceClient graphClient, str foreach (AadUserConversationMember member in members) { if (clientId == member.UserId && member.Roles.Contains("owner")) return true; - Debug.WriteLine(member); } } catch (ServiceException e) diff --git a/GraphWebApi/Controllers/GraphExplorerAppModeController.cs b/GraphWebApi/Controllers/GraphExplorerAppModeController.cs index a59a30ce7..bb10ea955 100644 --- a/GraphWebApi/Controllers/GraphExplorerAppModeController.cs +++ b/GraphWebApi/Controllers/GraphExplorerAppModeController.cs @@ -26,14 +26,12 @@ public class GraphExplorerAppModeController : ControllerBase { private readonly IGraphAppAuthProvider _graphAuthClient; private readonly ITokenAcquisition _tokenAcquisition; - private readonly IConfiguration _config; private readonly IGraphService _graphService; - public GraphExplorerAppModeController(IConfiguration configuration, ITokenAcquisition tokenAcquisition, IGraphAppAuthProvider graphServiceClient, IGraphService graphService) + public GraphExplorerAppModeController(ITokenAcquisition tokenAcquisition, IGraphAppAuthProvider graphServiceClient, IGraphService graphService) { this._graphAuthClient = graphServiceClient; this._tokenAcquisition = tokenAcquisition; - this._config = configuration; this._graphService = graphService; } @@ -225,7 +223,5 @@ public async Task ExecuteResultAsync(ActionContext context) } } } - - } } From 0e43543974be65033e2c378342d92ce70d29f7d3 Mon Sep 17 00:00:00 2001 From: Omer Abubaker Date: Wed, 4 Aug 2021 11:29:26 -0400 Subject: [PATCH 27/28] PR cleanup --- GraphExplorerAppModeService/Services/GraphService.cs | 6 +----- GraphWebApi/Controllers/GraphExplorerAppModeController.cs | 1 - 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/GraphExplorerAppModeService/Services/GraphService.cs b/GraphExplorerAppModeService/Services/GraphService.cs index 71bd927a0..cdb8e0f6a 100644 --- a/GraphExplorerAppModeService/Services/GraphService.cs +++ b/GraphExplorerAppModeService/Services/GraphService.cs @@ -18,11 +18,7 @@ public class GraphService : IGraphService /// /// ErrorMessage will contain the error message from the Graph Client call. /// - public string ErrorMessage - { - get { return errorMessage; } - set { errorMessage = value; } - } + public string ErrorMessage { get; set; } public async Task VerifyOwnership(GraphServiceClient graphClient, string query, string clientId) { diff --git a/GraphWebApi/Controllers/GraphExplorerAppModeController.cs b/GraphWebApi/Controllers/GraphExplorerAppModeController.cs index bb10ea955..eedaed85a 100644 --- a/GraphWebApi/Controllers/GraphExplorerAppModeController.cs +++ b/GraphWebApi/Controllers/GraphExplorerAppModeController.cs @@ -152,7 +152,6 @@ private async Task ProcessGraphRequest(string method, string all, contentType = contentTypes?.FirstOrDefault() ?? contentType; var byteArrayContent = await response.Content.ReadAsByteArrayAsync().ConfigureAwait(false); - Console.WriteLine(byteArrayContent); return new GraphResponse { From 294cdbdf91a896cd94ec250cab0bb392dff163d9 Mon Sep 17 00:00:00 2001 From: Omer Abubaker Date: Wed, 4 Aug 2021 16:54:13 -0400 Subject: [PATCH 28/28] Added PR changes and fixed error to return JSON --- GraphExplorerAppModeService/Interfaces/IGraphService.cs | 7 +++++++ GraphExplorerAppModeService/Services/GraphService.cs | 8 +++----- .../Controllers/GraphExplorerAppModeController.cs | 9 ++++++--- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/GraphExplorerAppModeService/Interfaces/IGraphService.cs b/GraphExplorerAppModeService/Interfaces/IGraphService.cs index 966bd4ca1..872078367 100644 --- a/GraphExplorerAppModeService/Interfaces/IGraphService.cs +++ b/GraphExplorerAppModeService/Interfaces/IGraphService.cs @@ -7,9 +7,16 @@ namespace GraphExplorerAppModeService.Interfaces { + /// + /// This class is for functions that call Microsoft Graph to check if the client owns the Teams resource they are trying to call against. + /// public interface IGraphService { + /// + /// ErrorMessage will contain the error message from the Graph Client call. + /// string ErrorMessage { get; set; } + Task VerifyOwnership(GraphServiceClient graphClient, string query, string clientId); } } diff --git a/GraphExplorerAppModeService/Services/GraphService.cs b/GraphExplorerAppModeService/Services/GraphService.cs index cdb8e0f6a..ebc1bac1b 100644 --- a/GraphExplorerAppModeService/Services/GraphService.cs +++ b/GraphExplorerAppModeService/Services/GraphService.cs @@ -15,9 +15,7 @@ public class GraphService : IGraphService { private string errorMessage; - /// - /// ErrorMessage will contain the error message from the Graph Client call. - /// + // Docs defined in IGraphService public string ErrorMessage { get; set; } public async Task VerifyOwnership(GraphServiceClient graphClient, string query, string clientId) @@ -26,10 +24,10 @@ public async Task VerifyOwnership(GraphServiceClient graphClient, string q for (int i=0; i < queryList.Length; i++) { - if (queryList[i] == "teams") + if (queryList[i] == "teams" && i + 1 < queryList.Length) { return await VerifyTeamsOwnership(graphClient, queryList[i + 1], clientId); - } else if (queryList[i] == "chats") + } else if (queryList[i] == "chats" && i + 1 < queryList.Length) { return await VerifyChatOwnership(graphClient, queryList[i + 1], clientId); } diff --git a/GraphWebApi/Controllers/GraphExplorerAppModeController.cs b/GraphWebApi/Controllers/GraphExplorerAppModeController.cs index eedaed85a..e818def75 100644 --- a/GraphWebApi/Controllers/GraphExplorerAppModeController.cs +++ b/GraphWebApi/Controllers/GraphExplorerAppModeController.cs @@ -18,6 +18,7 @@ using System.Security.Claims; using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Logging; +using Newtonsoft.Json; namespace GraphWebApi.Controllers { @@ -78,7 +79,6 @@ public async Task PatchAsync(string all, [FromBody] object body, { return await ProcessRequestAsync("PATCH", all, body, Authorization).ConfigureAwait(false); } - private async Task ProcessRequestAsync(string method, string all, object content, string Authorization) { // decode JWT Auth token @@ -108,12 +108,15 @@ private async Task ProcessRequestAsync(string method, string all, return new HttpResponseMessageResult(ReturnHttpResponseMessage(HttpStatusCode.OK, processedGraphRequest.contentType, new ByteArrayContent(processedGraphRequest.contentByteArray))); } else { - return new HttpResponseMessageResult(ReturnHttpResponseMessage(HttpStatusCode.Forbidden, errorContentType, new StringContent(""))); + Error error = new Error(); + error.Code = "Forbidden"; + error.Message = "The logged in user is not the owner of the resource."; + return new HttpResponseMessageResult(ReturnHttpResponseMessage(HttpStatusCode.Forbidden, errorContentType, new StringContent(JsonConvert.SerializeObject(error)))); } } catch (ServiceException ex) { - return new HttpResponseMessageResult(ReturnHttpResponseMessage(ex.StatusCode, errorContentType, new StringContent(ex.Error.ToString()))); + return new HttpResponseMessageResult(ReturnHttpResponseMessage(ex.StatusCode, errorContentType, new StringContent(JsonConvert.SerializeObject(ex.Error)))); } }