From 3d6e89460f063cc4ebebf050b8496611927d4077 Mon Sep 17 00:00:00 2001 From: Waldek Mastykarz Date: Wed, 21 Oct 2026 15:55:17 +0200 Subject: [PATCH] Exclude OIDC scopes from ApiCenterMinimalPermissionsPlugin excessive permissions ApiCenterMinimalPermissionsPlugin reported openid, profile, offline_access and email as excessive permissions, while the *MinimalPermissionsGuidance plugins exclude them. Add the same permissionsToExclude setting (with the same defaults) to ApiCenterMinimalPermissionsPlugin, list excluded permissions in its report, and update the config schema. Closes #1964 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: c925a02d-52bc-43fe-b416-00336c443309 --- .../ReportingPluginsIntegrationTests.cs | 40 ++++++++++++++++++- .../ApiCenterMinimalPermissionsPlugin.cs | 38 ++++++++++++++++-- ...ApiCenterMinimalPermissionsPluginReport.cs | 25 +++++++++++- ...centerminimalpermissionsplugin.schema.json | 8 ++++ 4 files changed, 105 insertions(+), 6 deletions(-) diff --git a/DevProxy.Integration.Tests/ReportingPluginsIntegrationTests.cs b/DevProxy.Integration.Tests/ReportingPluginsIntegrationTests.cs index 4d886c24c..35c0c9813 100644 --- a/DevProxy.Integration.Tests/ReportingPluginsIntegrationTests.cs +++ b/DevProxy.Integration.Tests/ReportingPluginsIntegrationTests.cs @@ -21,7 +21,8 @@ namespace DevProxy.Integration.Tests; /// /// NOT hermetic (deferred): GraphMinimalPermissions(+Guidance) call Microsoft Graph permission /// endpoints; MinimalCsomPermissions needs the shipped CSOM types definition + CSOM bodies; -/// ApiCenter* require Azure API Center. Those are integration-with-live-backend (Bucket 3). +/// ApiCenter* require Azure API Center. Those are integration-with-live-backend (Bucket 3); only +/// their backend-independent logic (permission exclusion, report rendering) is covered here. /// public sealed class ReportingPluginsIntegrationTests { @@ -150,4 +151,41 @@ await File.WriteAllTextAsync(Path.Combine(dir.FullName, "contoso.json"), """ dir.Delete(recursive: true); } } + + [Fact] + public void ApiCenterMinimalPermissions_ExcludesOidcScopesFromExcessivePermissions() + { + var excessive = ApiCenterMinimalPermissionsPlugin.GetExcessivePermissions( + tokenPermissions: ["User.Read", "User.Write", "openid", "profile", "offline_access", "email"], + minimalPermissions: ["User.Read"], + permissionsToExclude: ["profile", "openid", "offline_access", "email"]); + + Assert.Equal(["User.Write"], excessive); + } + + [Fact] + public void ApiCenterMinimalPermissions_WithoutExclusionsReportsAllExcessivePermissions() + { + var excessive = ApiCenterMinimalPermissionsPlugin.GetExcessivePermissions( + tokenPermissions: ["User.Read", "openid"], + minimalPermissions: ["User.Read"], + permissionsToExclude: null); + + Assert.Equal(["openid"], excessive); + } + + [Fact] + public void ApiCenterMinimalPermissionsReport_ListsExcludedPermissions() + { + var report = new ApiCenterMinimalPermissionsPluginReport + { + Results = [], + UnmatchedRequests = [], + Errors = [], + ExcludedPermissions = ["openid", "profile"], + }; + + Assert.Contains("- `openid`", report.ToMarkdown(), StringComparison.Ordinal); + Assert.Contains("openid, profile", report.ToPlainText(), StringComparison.Ordinal); + } } diff --git a/DevProxy.Plugins/Reporting/ApiCenterMinimalPermissionsPlugin.cs b/DevProxy.Plugins/Reporting/ApiCenterMinimalPermissionsPlugin.cs index 539182ac5..93e76c810 100644 --- a/DevProxy.Plugins/Reporting/ApiCenterMinimalPermissionsPlugin.cs +++ b/DevProxy.Plugins/Reporting/ApiCenterMinimalPermissionsPlugin.cs @@ -18,6 +18,7 @@ namespace DevProxy.Plugins.Reporting; public sealed class ApiCenterMinimalPermissionsPluginConfiguration { + public IEnumerable? PermissionsToExclude { get; set; } public string ResourceGroupName { get; set; } = ""; public string ServiceName { get; set; } = ""; public string SubscriptionId { get; set; } = ""; @@ -50,6 +51,8 @@ public override async Task InitializeAsync(InitArgs e, CancellationToken cancell await base.InitializeAsync(e, cancellationToken); + InitializePermissionsToExclude(); + try { _apiCenterClient = ActivatorUtilities.CreateInstance(e.ServiceProvider, @@ -132,6 +135,11 @@ public override async Task AfterRecordingStopAsync(RecordingArgs e, Cancellation var minimalPermissions = CheckMinimalPermissions(requests, apiDefinition); + var excessivePermissions = GetExcessivePermissions( + minimalPermissions.TokenPermissions, + minimalPermissions.MinimalScopes, + Configuration.PermissionsToExclude); + var api = _apis.FindApiByDefinition(apiDefinition, Logger); var result = new ApiCenterMinimalPermissionsPluginReportApiResult { @@ -143,8 +151,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(), SchemeName = Configuration.SchemeName }; results.Add(result); @@ -241,14 +249,38 @@ 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.IsEnabled(LogLevel.Information)) + { + Logger.LogInformation("Excluding the following permissions: {Permissions}", string.Join(", ", Configuration.PermissionsToExclude)); + } + StoreReport(report, e); Logger.LogTrace("Left {Name}", nameof(AfterRecordingStopAsync)); } + internal static IEnumerable GetExcessivePermissions( + IEnumerable tokenPermissions, + IEnumerable minimalPermissions, + IEnumerable? permissionsToExclude) => + [.. tokenPermissions + .Except(permissionsToExclude ?? []) + .Except(minimalPermissions)]; + + private void InitializePermissionsToExclude() + { + var key = nameof(ApiCenterMinimalPermissionsPluginConfiguration.PermissionsToExclude) + .ToCamelCase(); + + string[] defaultPermissionsToExclude = ["profile", "openid", "offline_access", "email"]; + Configuration.PermissionsToExclude = GetConfigurationValue(key, Configuration.PermissionsToExclude, defaultPermissionsToExclude); + } + private ApiPermissionsInfo CheckMinimalPermissions(IEnumerable requests, ApiDefinition apiDefinition) { if (Logger.IsEnabled(LogLevel.Information)) diff --git a/DevProxy.Plugins/Reporting/ApiCenterMinimalPermissionsPluginReport.cs b/DevProxy.Plugins/Reporting/ApiCenterMinimalPermissionsPluginReport.cs index 4a422a37f..8100199d8 100644 --- a/DevProxy.Plugins/Reporting/ApiCenterMinimalPermissionsPluginReport.cs +++ b/DevProxy.Plugins/Reporting/ApiCenterMinimalPermissionsPluginReport.cs @@ -27,6 +27,7 @@ public sealed class ApiCenterMinimalPermissionsPluginReport : IMarkdownReport, I public required IEnumerable Errors { get; init; } public required IEnumerable Results { get; init; } public required IEnumerable UnmatchedRequests { get; init; } + public IEnumerable? ExcludedPermissions { get; init; } public string ToMarkdown() { @@ -100,7 +101,17 @@ public string ToMarkdown() .OrderBy(o => o.Request) .Select(e => $"- `{e.Request}`: {e.Error}")).AppendLine() .AppendLine() - : sb.AppendLine("No errors occurred."); + : sb.AppendLine("No errors occurred.") + .AppendLine(); + + if (ExcludedPermissions?.Any() == true) + { + _ = sb.AppendLine("## 🚫 Excluded permissions") + .AppendLine() + .AppendLine("The following permissions were excluded from the analysis:") + .AppendLine() + .AppendJoin(Environment.NewLine, ExcludedPermissions.Select(p => $"- `{p}`")).AppendLine(); + } return sb.ToString(); } @@ -166,7 +177,17 @@ public string ToMarkdown() .OrderBy(o => o.Request) .Select(e => $"- `{e.Request}`: {e.Error}")).AppendLine() .AppendLine() - : sb.AppendLine("No errors occurred."); + : sb.AppendLine("No errors occurred.") + .AppendLine(); + + if (ExcludedPermissions?.Any() == true) + { + _ = sb.AppendLine("EXCLUDED PERMISSIONS") + .AppendLine() + .AppendLine("The following permissions were excluded from the analysis:") + .AppendLine() + .AppendJoin(", ", ExcludedPermissions).AppendLine(); + } return sb.ToString(); } diff --git a/schemas/v4.0.0/apicenterminimalpermissionsplugin.schema.json b/schemas/v4.0.0/apicenterminimalpermissionsplugin.schema.json index 582fe633a..2ff95ae65 100644 --- a/schemas/v4.0.0/apicenterminimalpermissionsplugin.schema.json +++ b/schemas/v4.0.0/apicenterminimalpermissionsplugin.schema.json @@ -7,6 +7,14 @@ "type": "string", "description": "The JSON schema reference for validation." }, + "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"] + }, "resourceGroupName": { "type": "string", "description": "Name of the resource group where the Azure API Center is located."