diff --git a/DevProxy.Plugins/Generation/MockGeneratorPlugin.cs b/DevProxy.Plugins/Generation/MockGeneratorPlugin.cs index 957e6564..27bad69d 100644 --- a/DevProxy.Plugins/Generation/MockGeneratorPlugin.cs +++ b/DevProxy.Plugins/Generation/MockGeneratorPlugin.cs @@ -7,7 +7,6 @@ using DevProxy.Abstractions.Proxy; using DevProxy.Abstractions.Utils; using DevProxy.Plugins.Mocking; -using DevProxy.Plugins.Utils; using Microsoft.Extensions.Logging; using System.Text.Json; using Titanium.Web.Proxy.EventArguments; @@ -34,7 +33,6 @@ public override async Task AfterRecordingStopAsync(RecordingArgs e, Cancellation Logger.LogInformation("Creating mocks from recorded requests..."); - var methodAndUrlComparer = new MethodAndUrlComparer(); var mocks = new List(); foreach (var request in e.RequestLogs) diff --git a/DevProxy.Plugins/Reporting/GraphMinimalPermissionsGuidancePlugin.cs b/DevProxy.Plugins/Reporting/GraphMinimalPermissionsGuidancePlugin.cs index 11ce07b9..1a89224d 100644 --- a/DevProxy.Plugins/Reporting/GraphMinimalPermissionsGuidancePlugin.cs +++ b/DevProxy.Plugins/Reporting/GraphMinimalPermissionsGuidancePlugin.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using DevProxy.Abstractions.Models; using DevProxy.Abstractions.Proxy; using DevProxy.Abstractions.Plugins; using DevProxy.Abstractions.Utils; @@ -77,9 +76,8 @@ public override async Task AfterRecordingStopAsync(RecordingArgs e, Cancellation return; } - var methodAndUrlComparer = new MethodAndUrlComparer(); - var delegatedEndpoints = new List<(string method, string url)>(); - var applicationEndpoints = new List<(string method, string url)>(); + var delegatedEndpoints = new List(); + var applicationEndpoints = new List(); // scope for delegated permissions IEnumerable scopesToEvaluate = []; @@ -94,21 +92,21 @@ public override async Task AfterRecordingStopAsync(RecordingArgs e, Cancellation } var methodAndUrlString = request.Message; - var methodAndUrl = GetMethodAndUrl(methodAndUrlString); - if (methodAndUrl.method.Equals("OPTIONS", StringComparison.OrdinalIgnoreCase)) + var methodAndUrl = MethodAndUrlUtils.GetMethodAndUrl(methodAndUrlString); + if (methodAndUrl.Method.Equals("OPTIONS", StringComparison.OrdinalIgnoreCase)) { continue; } - if (!ProxyUtils.MatchesUrlToWatch(UrlsToWatch, methodAndUrl.url)) + if (!ProxyUtils.MatchesUrlToWatch(UrlsToWatch, methodAndUrl.Url)) { - Logger.LogDebug("URL not matched: {Url}", methodAndUrl.url); + Logger.LogDebug("URL not matched: {Url}", methodAndUrl.Url); continue; } - var requestsFromBatch = Array.Empty<(string method, string url)>(); + var requestsFromBatch = Array.Empty(); - var uri = new Uri(methodAndUrl.url); + var uri = new Uri(methodAndUrl.Url); if (!ProxyUtils.IsGraphUrl(uri)) { continue; @@ -117,11 +115,11 @@ public override async Task AfterRecordingStopAsync(RecordingArgs e, Cancellation if (ProxyUtils.IsGraphBatchUrl(uri)) { var graphVersion = ProxyUtils.IsGraphBetaUrl(uri) ? "beta" : "v1.0"; - requestsFromBatch = GetRequestsFromBatch(request.Context?.Session.HttpClient.Request.BodyString!, graphVersion, uri.Host); + requestsFromBatch = GraphUtils.GetRequestsFromBatch(request.Context?.Session.HttpClient.Request.BodyString!, graphVersion, uri.Host); } else { - methodAndUrl = (methodAndUrl.method, GetTokenizedUrl(methodAndUrl.url)); + methodAndUrl = new(methodAndUrl.Method, GraphUtils.GetTokenizedUrl(methodAndUrl.Url)); } var (type, permissions) = GetPermissionsAndType(request); @@ -162,8 +160,8 @@ public override async Task AfterRecordingStopAsync(RecordingArgs e, Cancellation } // Remove duplicates - delegatedEndpoints = [.. delegatedEndpoints.Distinct(methodAndUrlComparer)]; - applicationEndpoints = [.. applicationEndpoints.Distinct(methodAndUrlComparer)]; + delegatedEndpoints = [.. delegatedEndpoints.Distinct()]; + applicationEndpoints = [.. applicationEndpoints.Distinct()]; if (delegatedEndpoints.Count == 0 && applicationEndpoints.Count == 0) { @@ -177,8 +175,7 @@ public override async Task AfterRecordingStopAsync(RecordingArgs e, Cancellation Logger.LogWarning("This plugin is in preview and may not return the correct results.\r\nPlease review the permissions and test your app before using them in production.\r\nIf you have any feedback, please open an issue at https://aka.ms/devproxy/issue.\r\n"); - if (Configuration.PermissionsToExclude is not null && - Configuration.PermissionsToExclude.Any()) + if (Configuration.PermissionsToExclude?.Any() == true) { Logger.LogInformation("Excluding the following permissions: {Permissions}", string.Join(", ", Configuration.PermissionsToExclude)); } @@ -188,7 +185,7 @@ public override async Task AfterRecordingStopAsync(RecordingArgs e, Cancellation var delegatedPermissionsInfo = new GraphMinimalPermissionsInfo(); report.DelegatedPermissions = delegatedPermissionsInfo; - Logger.LogInformation("Evaluating delegated permissions for: {Endpoints}", string.Join(", ", delegatedEndpoints.Select(e => $"{e.method} {e.url}"))); + Logger.LogInformation("Evaluating delegated permissions for: {Endpoints}", string.Join(", ", delegatedEndpoints.Select(e => $"{e.Method} {e.Url}"))); await EvaluateMinimalScopesAsync(delegatedEndpoints, scopesToEvaluate, GraphPermissionsType.Delegated, delegatedPermissionsInfo, cancellationToken); } @@ -198,7 +195,7 @@ public override async Task AfterRecordingStopAsync(RecordingArgs e, Cancellation var applicationPermissionsInfo = new GraphMinimalPermissionsInfo(); report.ApplicationPermissions = applicationPermissionsInfo; - Logger.LogInformation("Evaluating application permissions for: {Endpoints}", string.Join(", ", applicationEndpoints.Select(e => $"{e.method} {e.url}"))); + Logger.LogInformation("Evaluating application permissions for: {Endpoints}", string.Join(", ", applicationEndpoints.Select(e => $"{e.Method} {e.Url}"))); await EvaluateMinimalScopesAsync(applicationEndpoints, rolesToEvaluate, GraphPermissionsType.Application, applicationPermissionsInfo, cancellationToken); } @@ -218,7 +215,7 @@ private void InitializePermissionsToExclude() } private async Task EvaluateMinimalScopesAsync( - IEnumerable<(string method, string url)> endpoints, + IEnumerable endpoints, IEnumerable permissionsFromAccessToken, GraphPermissionsType scopeType, GraphMinimalPermissionsInfo permissionsInfo, @@ -229,12 +226,12 @@ private async Task EvaluateMinimalScopesAsync( throw new InvalidOperationException("GraphUtils is not initialized. Make sure to call InitializeAsync first."); } - var payload = endpoints.Select(e => new GraphRequestInfo { Method = e.method, Url = e.url }); + var payload = endpoints.Select(e => new GraphRequestInfo { Method = e.Method, Url = e.Url }); permissionsInfo.Operations = [.. endpoints.Select(e => new GraphMinimalPermissionsOperationInfo { - Method = e.method, - Endpoint = e.url + Method = e.Method, + Endpoint = e.Url })]; permissionsInfo.PermissionsFromTheToken = permissionsFromAccessToken; @@ -290,40 +287,6 @@ private async Task EvaluateMinimalScopesAsync( } } - private static (string method, string url)[] GetRequestsFromBatch(string batchBody, string graphVersion, string graphHostName) - { - var requests = new List<(string method, string url)>(); - - if (string.IsNullOrEmpty(batchBody)) - { - return [.. requests]; - } - - try - { - var batch = JsonSerializer.Deserialize(batchBody, ProxyUtils.JsonSerializerOptions); - if (batch == null) - { - return [.. requests]; - } - - foreach (var request in batch.Requests) - { - try - { - var method = request.Method; - var url = request.Url; - var absoluteUrl = $"https://{graphHostName}/{graphVersion}{url}"; - requests.Add((method, GetTokenizedUrl(absoluteUrl))); - } - catch { } - } - } - catch { } - - return [.. requests]; - } - /// /// Returns permissions and type (delegated or application) from the access token /// used on the request. @@ -377,20 +340,4 @@ private static (GraphPermissionsType type, IEnumerable permissions) GetP return (GraphPermissionsType.Application, []); } } - - private static (string method, string url) GetMethodAndUrl(string message) - { - var info = message.Split(" "); - if (info.Length > 2) - { - info = [info[0], string.Join(" ", info.Skip(1))]; - } - return (method: info[0], url: info[1]); - } - - private static string GetTokenizedUrl(string absoluteUrl) - { - var sanitizedUrl = ProxyUtils.SanitizeUrl(absoluteUrl); - return "/" + string.Join("", new Uri(sanitizedUrl).Segments.Skip(2).Select(Uri.UnescapeDataString)); - } } diff --git a/DevProxy.Plugins/Reporting/GraphMinimalPermissionsGuidancePluginReport.cs b/DevProxy.Plugins/Reporting/GraphMinimalPermissionsGuidancePluginReport.cs index 7f5800a2..dbdfdf84 100644 --- a/DevProxy.Plugins/Reporting/GraphMinimalPermissionsGuidancePluginReport.cs +++ b/DevProxy.Plugins/Reporting/GraphMinimalPermissionsGuidancePluginReport.cs @@ -62,8 +62,7 @@ void transformPermissionsInfo(GraphMinimalPermissionsInfo permissionsInfo, strin transformPermissionsInfo(ApplicationPermissions, "application"); } - if (ExcludedPermissions is not null && - ExcludedPermissions.Any()) + if (ExcludedPermissions?.Any() == true) { _ = sb.AppendLine("## Excluded permissions") .AppendLine() @@ -112,10 +111,9 @@ void transformPermissionsInfo(GraphMinimalPermissionsInfo permissionsInfo, strin transformPermissionsInfo(ApplicationPermissions, "Application"); } - if (ExcludedPermissions is not null && - ExcludedPermissions.Any()) + if (ExcludedPermissions?.Any() == true) { - _ = sb.AppendLine("Excluded: permissions:") + _ = sb.AppendLine("Excluded permissions:") .AppendLine() .AppendLine(string.Join(", ", ExcludedPermissions)); } diff --git a/DevProxy.Plugins/Reporting/GraphMinimalPermissionsPlugin.cs b/DevProxy.Plugins/Reporting/GraphMinimalPermissionsPlugin.cs index 24b3aac1..f20855b9 100644 --- a/DevProxy.Plugins/Reporting/GraphMinimalPermissionsPlugin.cs +++ b/DevProxy.Plugins/Reporting/GraphMinimalPermissionsPlugin.cs @@ -5,7 +5,6 @@ using DevProxy.Abstractions.Proxy; using DevProxy.Abstractions.Plugins; using DevProxy.Abstractions.Utils; -using DevProxy.Abstractions.Models; using DevProxy.Plugins.Models; using DevProxy.Plugins.Utils; using Microsoft.Extensions.Configuration; @@ -60,8 +59,7 @@ public override async Task AfterRecordingStopAsync(RecordingArgs e, Cancellation return; } - var methodAndUrlComparer = new MethodAndUrlComparer(); - var endpoints = new List<(string method, string url)>(); + var endpoints = new List(); foreach (var request in e.RequestLogs) { @@ -71,19 +69,19 @@ public override async Task AfterRecordingStopAsync(RecordingArgs e, Cancellation } var methodAndUrlString = request.Message; - var methodAndUrl = GetMethodAndUrl(methodAndUrlString); - if (methodAndUrl.method.Equals("OPTIONS", StringComparison.OrdinalIgnoreCase)) + var methodAndUrl = MethodAndUrlUtils.GetMethodAndUrl(methodAndUrlString); + if (methodAndUrl.Method.Equals("OPTIONS", StringComparison.OrdinalIgnoreCase)) { continue; } - if (!ProxyUtils.MatchesUrlToWatch(UrlsToWatch, methodAndUrl.url)) + if (!ProxyUtils.MatchesUrlToWatch(UrlsToWatch, methodAndUrl.Url)) { - Logger.LogDebug("URL not matched: {Url}", methodAndUrl.url); + Logger.LogDebug("URL not matched: {Url}", methodAndUrl.Url); continue; } - var uri = new Uri(methodAndUrl.url); + var uri = new Uri(methodAndUrl.Url); if (!ProxyUtils.IsGraphUrl(uri)) { continue; @@ -92,18 +90,18 @@ public override async Task AfterRecordingStopAsync(RecordingArgs e, Cancellation if (ProxyUtils.IsGraphBatchUrl(uri)) { var graphVersion = ProxyUtils.IsGraphBetaUrl(uri) ? "beta" : "v1.0"; - var requestsFromBatch = GetRequestsFromBatch(request.Context?.Session.HttpClient.Request.BodyString!, graphVersion, uri.Host); + var requestsFromBatch = GraphUtils.GetRequestsFromBatch(request.Context?.Session.HttpClient.Request.BodyString!, graphVersion, uri.Host); endpoints.AddRange(requestsFromBatch); } else { - methodAndUrl = (methodAndUrl.method, GetTokenizedUrl(methodAndUrl.url)); + methodAndUrl = new(methodAndUrl.Method, GraphUtils.GetTokenizedUrl(methodAndUrl.Url)); endpoints.Add(methodAndUrl); } } // Remove duplicates - endpoints = [.. endpoints.Distinct(methodAndUrlComparer)]; + endpoints = [.. endpoints.Distinct()]; if (endpoints.Count == 0) { @@ -111,7 +109,7 @@ public override async Task AfterRecordingStopAsync(RecordingArgs e, Cancellation return; } - Logger.LogInformation("Retrieving minimal permissions for:\r\n{Endpoints}\r\n", string.Join(Environment.NewLine, endpoints.Select(e => $"- {e.method} {e.url}"))); + Logger.LogInformation("Retrieving minimal permissions for:\r\n{Endpoints}\r\n", string.Join(Environment.NewLine, endpoints.Select(e => $"- {e.Method} {e.Url}"))); Logger.LogWarning("This plugin is in preview and may not return the correct results.\r\nPlease review the permissions and test your app before using them in production.\r\nIf you have any feedback, please open an issue at https://aka.ms/devproxy/issue.\r\n"); @@ -125,7 +123,7 @@ public override async Task AfterRecordingStopAsync(RecordingArgs e, Cancellation } private async Task DetermineMinimalScopesAsync( - IEnumerable<(string method, string url)> endpoints, + IEnumerable endpoints, CancellationToken cancellationToken) { if (_graphUtils is null) @@ -133,7 +131,7 @@ public override async Task AfterRecordingStopAsync(RecordingArgs e, Cancellation throw new InvalidOperationException("GraphUtils is not initialized. Make sure to call InitializeAsync first."); } - var payload = endpoints.Select(e => new GraphRequestInfo { Method = e.method, Url = e.url }); + var payload = endpoints.Select(e => new GraphRequestInfo { Method = e.Method, Url = e.Url }); try { @@ -178,54 +176,4 @@ public override async Task AfterRecordingStopAsync(RecordingArgs e, Cancellation return null; } } - - private static (string method, string url)[] GetRequestsFromBatch(string batchBody, string graphVersion, string graphHostName) - { - var requests = new List<(string, string)>(); - - if (string.IsNullOrEmpty(batchBody)) - { - return [.. requests]; - } - - try - { - var batch = JsonSerializer.Deserialize(batchBody, ProxyUtils.JsonSerializerOptions); - if (batch == null) - { - return [.. requests]; - } - - foreach (var request in batch.Requests) - { - try - { - var method = request.Method; - var url = request.Url; - var absoluteUrl = $"https://{graphHostName}/{graphVersion}{url}"; - requests.Add((method, GetTokenizedUrl(absoluteUrl))); - } - catch { } - } - } - catch { } - - return [.. requests]; - } - - private static (string method, string url) GetMethodAndUrl(string message) - { - var info = message.Split(" "); - if (info.Length > 2) - { - info = [info[0], string.Join(" ", info.Skip(1))]; - } - return (info[0], info[1]); - } - - private static string GetTokenizedUrl(string absoluteUrl) - { - var sanitizedUrl = ProxyUtils.SanitizeUrl(absoluteUrl); - return "/" + string.Join("", new Uri(sanitizedUrl).Segments.Skip(2).Select(Uri.UnescapeDataString)); - } } diff --git a/DevProxy.Plugins/Reporting/MinimalPermissionsGuidancePlugin.cs b/DevProxy.Plugins/Reporting/MinimalPermissionsGuidancePlugin.cs index 59d9ff9b..162e67f6 100644 --- a/DevProxy.Plugins/Reporting/MinimalPermissionsGuidancePlugin.cs +++ b/DevProxy.Plugins/Reporting/MinimalPermissionsGuidancePlugin.cs @@ -28,11 +28,13 @@ public sealed class MinimalPermissionsGuidancePluginReport public required IEnumerable Errors { get; init; } public required IEnumerable Results { get; init; } public required IEnumerable UnmatchedRequests { get; init; } + public IEnumerable? ExcludedPermissions { get; set; } } public sealed class MinimalPermissionsGuidancePluginConfiguration { public string? ApiSpecsFolderPath { get; set; } + public IEnumerable? PermissionsToExclude { get; set; } } public sealed class MinimalPermissionsGuidancePlugin( @@ -56,17 +58,8 @@ public override async Task InitializeAsync(InitArgs e, CancellationToken cancell { await base.InitializeAsync(e, cancellationToken); - if (string.IsNullOrWhiteSpace(Configuration.ApiSpecsFolderPath)) - { - Enabled = false; - throw new InvalidOperationException("ApiSpecsFolderPath is required."); - } - Configuration.ApiSpecsFolderPath = ProxyUtils.GetFullPath(Configuration.ApiSpecsFolderPath, ProxyConfiguration.ConfigFile); - if (!Path.Exists(Configuration.ApiSpecsFolderPath)) - { - Enabled = false; - throw new InvalidOperationException($"ApiSpecsFolderPath '{Configuration.ApiSpecsFolderPath}' does not exist."); - } + InitializeApiSpecsFolderPath(); + InitializePermissionsToExclude(); } public override async Task AfterRecordingStopAsync(RecordingArgs e, CancellationToken cancellationToken) @@ -110,6 +103,11 @@ public override async Task AfterRecordingStopAsync(RecordingArgs e, Cancellation { var minimalPermissions = apiSpec.CheckMinimalPermissions(requests, Logger); + IEnumerable excessivePermissions = [.. minimalPermissions.TokenPermissions + .Except(Configuration.PermissionsToExclude ?? []) + .Except(minimalPermissions.MinimalScopes) + ]; + var result = new MinimalPermissionsGuidancePluginReportApiResult { ApiName = GetApiName(minimalPermissions.OperationsFromRequests.Any() ? @@ -119,8 +117,8 @@ public override async Task AfterRecordingStopAsync(RecordingArgs e, Cancellation .Distinct()], TokenPermissions = [.. minimalPermissions.TokenPermissions.Distinct()], MinimalPermissions = minimalPermissions.MinimalScopes, - ExcessivePermissions = [.. minimalPermissions.TokenPermissions.Except(minimalPermissions.MinimalScopes)], - UsesMinimalPermissions = !minimalPermissions.TokenPermissions.Except(minimalPermissions.MinimalScopes).Any() + ExcessivePermissions = excessivePermissions, + UsesMinimalPermissions = !excessivePermissions.Any() }; results.Add(result); @@ -173,14 +171,45 @@ public override async Task AfterRecordingStopAsync(RecordingArgs e, Cancellation { Results = [.. results], UnmatchedRequests = [.. unmatchedRequests], - Errors = [.. errors] + Errors = [.. errors], + ExcludedPermissions = Configuration.PermissionsToExclude }; + if (Configuration.PermissionsToExclude?.Any() == true) + { + Logger.LogInformation("Excluding the following permissions: {Permissions}", string.Join(", ", Configuration.PermissionsToExclude)); + } + StoreReport(report, e); Logger.LogTrace("Left {Name}", nameof(AfterRecordingStopAsync)); } + private void InitializeApiSpecsFolderPath() + { + if (string.IsNullOrWhiteSpace(Configuration.ApiSpecsFolderPath)) + { + Enabled = false; + throw new InvalidOperationException("ApiSpecsFolderPath is required."); + } + + Configuration.ApiSpecsFolderPath = ProxyUtils.GetFullPath(Configuration.ApiSpecsFolderPath, ProxyConfiguration.ConfigFile); + if (!Path.Exists(Configuration.ApiSpecsFolderPath)) + { + Enabled = false; + throw new FileNotFoundException($"ApiSpecsFolderPath '{Configuration.ApiSpecsFolderPath}' does not exist."); + } + } + + private void InitializePermissionsToExclude() + { + var key = nameof(MinimalPermissionsGuidancePluginConfiguration.PermissionsToExclude) + .ToCamelCase(); + + string[] defaultPermissionsToExclude = ["profile", "openid", "offline_access", "email"]; + Configuration.PermissionsToExclude = GetConfigurationValue(key, Configuration.PermissionsToExclude, defaultPermissionsToExclude); + } + private async Task> LoadApiSpecsAsync(string apiSpecsFolderPath, CancellationToken cancellationToken) { var apiDefinitions = new Dictionary(); diff --git a/DevProxy.Plugins/Utils/GraphUtils.cs b/DevProxy.Plugins/Utils/GraphUtils.cs index 27ef95b6..986f708e 100644 --- a/DevProxy.Plugins/Utils/GraphUtils.cs +++ b/DevProxy.Plugins/Utils/GraphUtils.cs @@ -2,9 +2,12 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using DevProxy.Abstractions.Models; +using DevProxy.Abstractions.Utils; using DevProxy.Plugins.Models; using Microsoft.Extensions.Logging; using System.Net.Http.Json; +using System.Text.Json; using Titanium.Web.Proxy.Http; namespace DevProxy.Plugins.Utils; @@ -47,9 +50,9 @@ internal static string GetScopeTypeString(GraphPermissionsType type) }; } - internal async Task> UpdateUserScopesAsync(IEnumerable minimalScopes, IEnumerable<(string method, string url)> endpoints, GraphPermissionsType permissionsType) + internal async Task> UpdateUserScopesAsync(IEnumerable minimalScopes, IEnumerable endpoints, GraphPermissionsType permissionsType) { - var userEndpoints = endpoints.Where(e => e.url.Contains("/users/{", StringComparison.OrdinalIgnoreCase)); + var userEndpoints = endpoints.Where(e => e.Url.Contains("/users/{", StringComparison.OrdinalIgnoreCase)); if (!userEndpoints.Any()) { return minimalScopes; @@ -60,8 +63,8 @@ internal async Task> UpdateUserScopesAsync(IEnumerable { - _logger.LogDebug("Getting permissions for {Method} {Url}", e.method, e.url); - return $"{url}&requesturl={e.url}&method={e.method}"; + _logger.LogDebug("Getting permissions for {Method} {Url}", e.Method, e.Url); + return $"{url}&requesturl={e.Url}&method={e.Method}"; }); var tasks = urls.Select(u => { @@ -96,4 +99,45 @@ internal async Task> UpdateUserScopesAsync(IEnumerable(); + + if (string.IsNullOrEmpty(batchBody)) + { + return [.. requests]; + } + + try + { + var batch = JsonSerializer.Deserialize(batchBody, ProxyUtils.JsonSerializerOptions); + if (batch == null) + { + return [.. requests]; + } + + foreach (var request in batch.Requests) + { + try + { + var method = request.Method; + var url = request.Url; + var absoluteUrl = $"https://{graphHostName}/{graphVersion}{url}"; + MethodAndUrl methodAndUrl = new(Method: method, Url: GetTokenizedUrl(absoluteUrl)); + requests.Add(methodAndUrl); + } + catch { } + } + } + catch { } + + return [.. requests]; + } } \ No newline at end of file diff --git a/DevProxy.Plugins/Utils/MethodAndUrlComparer.cs b/DevProxy.Plugins/Utils/MethodAndUrlComparer.cs deleted file mode 100644 index abf10515..00000000 --- a/DevProxy.Plugins/Utils/MethodAndUrlComparer.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -namespace DevProxy.Plugins.Utils; - -internal sealed class MethodAndUrlComparer : IEqualityComparer<(string method, string url)> -{ - public bool Equals((string method, string url) x, (string method, string url) y) => - x.method == y.method && x.url == y.url; - - public int GetHashCode((string method, string url) obj) - { - var methodHashCode = obj.method.GetHashCode(StringComparison.OrdinalIgnoreCase); - var urlHashCode = obj.url.GetHashCode(StringComparison.OrdinalIgnoreCase); - - return methodHashCode ^ urlHashCode; - } -} \ No newline at end of file diff --git a/DevProxy.Plugins/Utils/MethodAndUrlUtils.cs b/DevProxy.Plugins/Utils/MethodAndUrlUtils.cs new file mode 100644 index 00000000..b4412638 --- /dev/null +++ b/DevProxy.Plugins/Utils/MethodAndUrlUtils.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace DevProxy.Plugins.Utils; + +internal readonly record struct MethodAndUrl(string Method, string Url); + +static class MethodAndUrlUtils +{ + internal static MethodAndUrl GetMethodAndUrl(string methodAndUrlString) + { + ArgumentException.ThrowIfNullOrWhiteSpace(methodAndUrlString); + + var info = methodAndUrlString.Split(" "); + if (info.Length > 2) + { + info = [info[0], string.Join(" ", info.Skip(1))]; + } + return new(Method: info[0], Url: info[1]); + } +} \ No newline at end of file diff --git a/schemas/v1.1.0/minimalpermissionsguidanceplugin.schema.json b/schemas/v1.1.0/minimalpermissionsguidanceplugin.schema.json index 67849ad6..d9f54eed 100644 --- a/schemas/v1.1.0/minimalpermissionsguidanceplugin.schema.json +++ b/schemas/v1.1.0/minimalpermissionsguidanceplugin.schema.json @@ -10,6 +10,14 @@ "apiSpecsFolderPath": { "type": "string", "description": "Relative or absolute path to the folder with API specs. Used to compare JWT token permissions against minimal required scopes." + }, + "permissionsToExclude": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The scopes to ignore and not include in the report. Default: ['profile', 'openid', 'offline_access', 'email'].", + "default": ["profile", "openid", "offline_access", "email"] } }, "required": [