From 4d802c1b63b05d4aa80cecf562466b5f1e772e0b Mon Sep 17 00:00:00 2001 From: Waldek Mastykarz Date: Fri, 21 Jun 2024 14:08:40 +0200 Subject: [PATCH] Fixes determining minimal Microsoft Graph permissions (#805) --- dev-proxy-plugins/GraphUtils.cs | 62 +++++++++++++++++++ .../MinimalPermissionsGuidancePlugin.cs | 18 +++--- .../RequestLogs/MinimalPermissionsPlugin.cs | 17 ++--- 3 files changed, 75 insertions(+), 22 deletions(-) diff --git a/dev-proxy-plugins/GraphUtils.cs b/dev-proxy-plugins/GraphUtils.cs index 4de6bd40..9341427c 100644 --- a/dev-proxy-plugins/GraphUtils.cs +++ b/dev-proxy-plugins/GraphUtils.cs @@ -1,6 +1,9 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.Net.Http.Json; +using Microsoft.DevProxy.Plugins.RequestLogs.MinimalPermissions; +using Microsoft.Extensions.Logging; using Titanium.Web.Proxy.Http; namespace Microsoft.DevProxy.Plugins; @@ -27,4 +30,63 @@ public static string BuildThrottleKey(Uri uri) return workload; } + + internal static string GetScopeTypeString(PermissionsType type) + { + return type switch + { + PermissionsType.Application => "Application", + PermissionsType.Delegated => "DelegatedWork", + _ => throw new InvalidOperationException($"Unknown scope type: {type}") + }; + } + + internal static async Task UpdateUserScopes(string[] minimalScopes, IEnumerable<(string method, string url)> endpoints, PermissionsType permissionsType, ILogger logger) + { + var userEndpoints = endpoints.Where(e => e.url.Contains("/users/{", StringComparison.OrdinalIgnoreCase)); + if (!userEndpoints.Any()) + { + return minimalScopes; + } + + var newMinimalScopes = new HashSet(minimalScopes); + + var url = $"https://graphexplorerapi.azurewebsites.net/permissions?scopeType={GetScopeTypeString(permissionsType)}"; + using var httpClient = new HttpClient(); + var urls = userEndpoints.Select(e => { + logger.LogDebug("Getting permissions for {method} {url}", e.method, e.url); + return $"{url}&requesturl={e.url}&method={e.method}"; + }); + var tasks = urls.Select(u => { + logger.LogTrace("Calling {url}...", u); + return httpClient.GetFromJsonAsync(u); + }); + await Task.WhenAll(tasks); + + foreach (var task in tasks) + { + var response = await task; + if (response is null) + { + continue; + } + + // there's only one scope so it must be minimal already + if (response.Length < 2) + { + continue; + } + + if (newMinimalScopes.Contains(response[0].Value)) + { + logger.LogDebug("Replacing scope {old} with {new}", response[0].Value, response[1].Value); + newMinimalScopes.Remove(response[0].Value); + newMinimalScopes.Add(response[1].Value); + } + } + + logger.LogDebug("Updated minimal scopes. Original: {original}, New: {new}", string.Join(", ", minimalScopes), string.Join(", ", newMinimalScopes)); + + return newMinimalScopes.ToArray(); + } } \ No newline at end of file diff --git a/dev-proxy-plugins/RequestLogs/MinimalPermissionsGuidancePlugin.cs b/dev-proxy-plugins/RequestLogs/MinimalPermissionsGuidancePlugin.cs index 636ef6e8..b6efca57 100644 --- a/dev-proxy-plugins/RequestLogs/MinimalPermissionsGuidancePlugin.cs +++ b/dev-proxy-plugins/RequestLogs/MinimalPermissionsGuidancePlugin.cs @@ -252,16 +252,6 @@ private async Task AfterRecordingStop(object? sender, RecordingArgs e) } } - private string GetScopeTypeString(PermissionsType scopeType) - { - return scopeType switch - { - PermissionsType.Application => "Application", - PermissionsType.Delegated => "DelegatedWork", - _ => throw new InvalidOperationException($"Unknown scope type: {scopeType}") - }; - } - private async Task EvaluateMinimalScopes(IEnumerable<(string method, string url)> endpoints, string[] permissionsFromAccessToken, PermissionsType scopeType, MinimalPermissionsInfo permissionsInfo) { var payload = endpoints.Select(e => new RequestInfo { Method = e.method, Url = e.url }); @@ -275,7 +265,7 @@ private async Task EvaluateMinimalScopes(IEnumerable<(string method, string url) try { - var url = $"https://graphexplorerapi.azurewebsites.net/permissions?scopeType={GetScopeTypeString(scopeType)}"; + var url = $"https://graphexplorerapi.azurewebsites.net/permissions?scopeType={GraphUtils.GetScopeTypeString(scopeType)}"; using var client = new HttpClient(); var stringPayload = JsonSerializer.Serialize(payload, ProxyUtils.JsonSerializerOptions); Logger.LogDebug(string.Format("Calling {0} with payload{1}{2}", url, Environment.NewLine, stringPayload)); @@ -288,6 +278,12 @@ private async Task EvaluateMinimalScopes(IEnumerable<(string method, string url) var resultsAndErrors = JsonSerializer.Deserialize(content, ProxyUtils.JsonSerializerOptions); var minimalPermissions = resultsAndErrors?.Results?.Select(p => p.Value).ToArray() ?? Array.Empty(); var errors = resultsAndErrors?.Errors?.Select(e => $"- {e.Url} ({e.Message})") ?? Array.Empty(); + + if (scopeType == PermissionsType.Delegated) + { + minimalPermissions = await GraphUtils.UpdateUserScopes(minimalPermissions, endpoints, scopeType, Logger); + } + if (minimalPermissions.Any()) { var excessPermissions = permissionsFromAccessToken diff --git a/dev-proxy-plugins/RequestLogs/MinimalPermissionsPlugin.cs b/dev-proxy-plugins/RequestLogs/MinimalPermissionsPlugin.cs index 26dc086e..9dca71c3 100644 --- a/dev-proxy-plugins/RequestLogs/MinimalPermissionsPlugin.cs +++ b/dev-proxy-plugins/RequestLogs/MinimalPermissionsPlugin.cs @@ -136,23 +136,13 @@ private async Task AfterRecordingStop(object? sender, RecordingArgs e) return requests.ToArray(); } - private string GetScopeTypeString() - { - return _configuration.Type switch - { - PermissionsType.Application => "Application", - PermissionsType.Delegated => "DelegatedWork", - _ => throw new InvalidOperationException($"Unknown scope type: {_configuration.Type}") - }; - } - private async Task DetermineMinimalScopes(IEnumerable<(string method, string url)> endpoints) { var payload = endpoints.Select(e => new RequestInfo { Method = e.method, Url = e.url }); try { - var url = $"https://graphexplorerapi.azurewebsites.net/permissions?scopeType={GetScopeTypeString()}"; + var url = $"https://graphexplorerapi.azurewebsites.net/permissions?scopeType={GraphUtils.GetScopeTypeString(_configuration.Type)}"; using var client = new HttpClient(); var stringPayload = JsonSerializer.Serialize(payload, ProxyUtils.JsonSerializerOptions); Logger.LogDebug("Calling {url} with payload\r\n{stringPayload}", url, stringPayload); @@ -166,6 +156,11 @@ private string GetScopeTypeString() var minimalScopes = resultsAndErrors?.Results?.Select(p => p.Value).ToArray() ?? Array.Empty(); var errors = resultsAndErrors?.Errors?.Select(e => $"- {e.Url} ({e.Message})") ?? Array.Empty(); + if (_configuration.Type == PermissionsType.Delegated) + { + minimalScopes = await GraphUtils.UpdateUserScopes(minimalScopes, endpoints, _configuration.Type, Logger); + } + if (minimalScopes.Any()) { Logger.LogInformation("Minimal permissions:\r\n{permissions}", string.Join(", ", minimalScopes));