Skip to content

Extend MinimalPermissionsGuidancePlugin with scopes to ignore #1365

New issue

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

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

Already on GitHub? Sign in to your account

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion DevProxy.Plugins/Generation/MockGeneratorPlugin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ public override async Task AfterRecordingStopAsync(RecordingArgs e, Cancellation

Logger.LogInformation("Creating mocks from recorded requests...");

var methodAndUrlComparer = new MethodAndUrlComparer();
var mocks = new List<MockResponse>();

foreach (var request in e.RequestLogs)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,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<MethodAndUrl>();
var applicationEndpoints = new List<MethodAndUrl>();

// scope for delegated permissions
IEnumerable<string> scopesToEvaluate = [];
Expand All @@ -94,21 +93,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<MethodAndUrl>();

var uri = new Uri(methodAndUrl.url);
var uri = new Uri(methodAndUrl.Url);
if (!ProxyUtils.IsGraphUrl(uri))
{
continue;
Expand All @@ -117,11 +116,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);
Expand Down Expand Up @@ -162,8 +161,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)
{
Expand All @@ -177,8 +176,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));
}
Expand All @@ -188,7 +186,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);
}
Expand All @@ -198,7 +196,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);
}
Expand All @@ -218,7 +216,7 @@ private void InitializePermissionsToExclude()
}

private async Task EvaluateMinimalScopesAsync(
IEnumerable<(string method, string url)> endpoints,
IEnumerable<MethodAndUrl> endpoints,
IEnumerable<string> permissionsFromAccessToken,
GraphPermissionsType scopeType,
GraphMinimalPermissionsInfo permissionsInfo,
Expand All @@ -229,12 +227,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;

Expand Down Expand Up @@ -290,40 +288,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<GraphBatchRequestPayload>(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];
}

/// <summary>
/// Returns permissions and type (delegated or application) from the access token
/// used on the request.
Expand Down Expand Up @@ -377,20 +341,4 @@ private static (GraphPermissionsType type, IEnumerable<string> 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));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -112,8 +111,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()
Expand Down
75 changes: 12 additions & 63 deletions DevProxy.Plugins/Reporting/GraphMinimalPermissionsPlugin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,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<MethodAndUrl>();

foreach (var request in e.RequestLogs)
{
Expand All @@ -71,19 +70,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;
Expand All @@ -92,26 +91,26 @@ 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)
{
Logger.LogInformation("No requests to Microsoft Graph endpoints recorded. Will not retrieve minimal permissions.");
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");

Expand All @@ -125,15 +124,15 @@ public override async Task AfterRecordingStopAsync(RecordingArgs e, Cancellation
}

private async Task<GraphMinimalPermissionsPluginReport?> DetermineMinimalScopesAsync(
IEnumerable<(string method, string url)> endpoints,
IEnumerable<MethodAndUrl> endpoints,
CancellationToken cancellationToken)
{
if (_graphUtils is null)
{
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
{
Expand Down Expand Up @@ -178,54 +177,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<GraphBatchRequestPayload>(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));
}
}
Loading