Skip to content

Commit a63660a

Browse files
Adds the ApiCenterMinimalPermissionsPlugin (#761)
* Refactors APIC plugins * Adds the ApiCenterMinimalPermissionsPlugin
1 parent 0ce161d commit a63660a

File tree

6 files changed

+644
-6
lines changed

6 files changed

+644
-6
lines changed

dev-proxy-plugins/ApiCenter/ModelExtensions.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,12 @@ internal static async Task<Dictionary<string, ApiDefinition>> GetApiDefinitionsB
237237
}
238238
}
239239

240+
logger.LogDebug(
241+
"Loaded API definitions from API Center for APIs:{newLine}- {apis}",
242+
Environment.NewLine,
243+
string.Join($"{Environment.NewLine}- ", apiDefinitions.Keys)
244+
);
245+
240246
return apiDefinitions;
241247
}
242248

@@ -259,4 +265,22 @@ internal static async Task<Dictionary<string, ApiDefinition>> GetApiDefinitionsB
259265
return api.Api;
260266
}
261267
}
268+
269+
internal static Api? FindApiByDefinition(this Api[] apis, ApiDefinition apiDefinition, ILogger logger)
270+
{
271+
var api = apis
272+
.FirstOrDefault(a =>
273+
(a.Versions?.Any(v => v.Definitions?.Any(d => d.Id == apiDefinition.Id) == true) == true) ||
274+
(a.Deployments?.Any(d => d.Properties?.DefinitionId == apiDefinition.Id) == true));
275+
if (api is null)
276+
{
277+
logger.LogDebug("No matching API found for {apiDefinitionId}", apiDefinition.Id);
278+
return null;
279+
}
280+
else
281+
{
282+
logger.LogDebug("API {api} found for {apiDefinitionId}", api.Name, apiDefinition.Id);
283+
return api;
284+
}
285+
}
262286
}

dev-proxy-plugins/OpenApi/OpenApiDocumentExtensions.cs

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ namespace Microsoft.OpenApi.Models;
88

99
public static class OpenApiDocumentExtensions
1010
{
11-
public static OpenApiPathItem? FindMatchingPathItem(this OpenApiDocument openApiDocument, string requestUrl, ILogger logger)
11+
public static KeyValuePair<string, OpenApiPathItem>? FindMatchingPathItem(this OpenApiDocument openApiDocument, string requestUrl, ILogger logger)
1212
{
1313
foreach (var server in openApiDocument.Servers)
1414
{
@@ -50,7 +50,7 @@ public static class OpenApiDocumentExtensions
5050
{
5151
logger.LogDebug("Regex matches {requestUrl}", urlPathFromRequest);
5252

53-
return path.Value;
53+
return path;
5454
}
5555

5656
logger.LogDebug("Regex does not match {requestUrl}", urlPathFromRequest);
@@ -60,8 +60,7 @@ public static class OpenApiDocumentExtensions
6060
if (urlPathFromRequest.Equals(urlPathFromSpec, StringComparison.OrdinalIgnoreCase))
6161
{
6262
logger.LogDebug("{requestUrl} matches {urlPath}", requestUrl, urlPathFromSpec);
63-
64-
return path.Value;
63+
return path;
6564
}
6665

6766
logger.LogDebug("{requestUrl} doesn't match {urlPath}", requestUrl, urlPathFromSpec);
@@ -71,4 +70,46 @@ public static class OpenApiDocumentExtensions
7170

7271
return null;
7372
}
73+
74+
public static string[] GetEffectiveScopes(this OpenApiOperation operation, OpenApiDocument openApiDocument, ILogger logger)
75+
{
76+
var oauth2Scheme = openApiDocument.GetOAuth2Schemes().FirstOrDefault();
77+
if (oauth2Scheme is null)
78+
{
79+
logger.LogDebug("No OAuth2 schemes found in OpenAPI document");
80+
return [];
81+
}
82+
83+
var globalScopes = new string[] { };
84+
var globalOAuth2Requirement = openApiDocument.SecurityRequirements
85+
.FirstOrDefault(req => req.ContainsKey(oauth2Scheme));
86+
if (globalOAuth2Requirement is not null)
87+
{
88+
globalScopes = [.. globalOAuth2Requirement[oauth2Scheme]];
89+
}
90+
91+
if (operation.Security is null)
92+
{
93+
logger.LogDebug("No security requirements found in operation {operation}", operation.OperationId);
94+
return globalScopes;
95+
}
96+
97+
var operationOAuth2Requirement = operation.Security
98+
.Where(req => req.ContainsKey(oauth2Scheme))
99+
.SelectMany(req => req[oauth2Scheme]);
100+
if (operationOAuth2Requirement is not null)
101+
{
102+
return operationOAuth2Requirement.ToArray();
103+
}
104+
105+
return [];
106+
}
107+
108+
public static OpenApiSecurityScheme[] GetOAuth2Schemes(this OpenApiDocument openApiDocument)
109+
{
110+
return openApiDocument.Components.SecuritySchemes
111+
.Where(s => s.Value.Type == SecuritySchemeType.OAuth2)
112+
.Select(s => s.Value)
113+
.ToArray();
114+
}
74115
}

dev-proxy-plugins/Reporters/MarkdownReporter.cs

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ public class MarkdownReporter : BaseReporter
1616

1717
private readonly Dictionary<Type, Func<object, string?>> _transformers = new()
1818
{
19+
{ typeof(ApiCenterMinimalPermissionsPluginReport), TransformApiCenterMinimalPermissionsReport },
1920
{ typeof(ApiCenterOnboardingPluginReport), TransformApiCenterOnboardingReport },
2021
{ typeof(ApiCenterProductionVersionPluginReport), TransformApiCenterProductionVersionReport },
2122
{ typeof(ExecutionSummaryPluginReportByUrl), TransformExecutionSummaryByUrl },
@@ -103,6 +104,92 @@ public MarkdownReporter(IPluginEvents pluginEvents, IProxyContext context, ILogg
103104
return sb.ToString();
104105
}
105106

107+
private static string? TransformApiCenterMinimalPermissionsReport(object report)
108+
{
109+
var apiCenterMinimalPermissionsReport = (ApiCenterMinimalPermissionsPluginReport)report;
110+
111+
var sb = new StringBuilder();
112+
sb.AppendLine("# Azure API Center minimal permissions report")
113+
.AppendLine();
114+
115+
sb.AppendLine("## ℹ️ Summary")
116+
.AppendLine()
117+
.AppendLine("<table>")
118+
.AppendFormat("<tr><td>🔎 APIs inspected</td><td align=\"right\">{0}</td></tr>{1}", apiCenterMinimalPermissionsReport.Results.Length, Environment.NewLine)
119+
.AppendFormat("<tr><td>🔎 Requests inspected</td><td align=\"right\">{0}</td></tr>{1}", apiCenterMinimalPermissionsReport.Results.Sum(r => r.Requests.Length), Environment.NewLine)
120+
.AppendFormat("<tr><td>✅ APIs called using minimal permissions</td><td align=\"right\">{0}</td></tr>{1}", apiCenterMinimalPermissionsReport.Results.Count(r => r.UsesMinimalPermissions), Environment.NewLine)
121+
.AppendFormat("<tr><td>🛑 APIs called using excessive permissions</td><td align=\"right\">{0}</td></tr>{1}", apiCenterMinimalPermissionsReport.Results.Count(r => !r.UsesMinimalPermissions), Environment.NewLine)
122+
.AppendFormat("<tr><td>⚠️ Unmatched requests</td><td align=\"right\">{0}</td></tr>{1}", apiCenterMinimalPermissionsReport.UnmatchedRequests.Length, Environment.NewLine)
123+
.AppendFormat("<tr><td>🛑 Errors</td><td align=\"right\">{0}</td></tr>{1}", apiCenterMinimalPermissionsReport.Errors.Length, Environment.NewLine)
124+
.AppendLine("</table>")
125+
.AppendLine();
126+
127+
sb.AppendLine("## 🔌 APIs")
128+
.AppendLine();
129+
130+
if (apiCenterMinimalPermissionsReport.Results.Any())
131+
{
132+
foreach (var apiResult in apiCenterMinimalPermissionsReport.Results)
133+
{
134+
sb.AppendFormat("### {0}{1}", apiResult.ApiName, Environment.NewLine)
135+
.AppendLine()
136+
.AppendFormat(apiResult.UsesMinimalPermissions ? "✅ Called using minimal permissions{0}" : "🛑 Called using excessive permissions{0}", Environment.NewLine)
137+
.AppendLine()
138+
.AppendLine("#### Permissions")
139+
.AppendLine()
140+
.AppendFormat("- Minimal permissions: {0}{1}", string.Join(", ", apiResult.MinimalPermissions.Order().Select(p => $"`{p}`")), Environment.NewLine)
141+
.AppendFormat("- Permissions on the token: {0}{1}", string.Join(", ", apiResult.TokenPermissions.Order().Select(p => $"`{p}`")), Environment.NewLine)
142+
.AppendFormat("- Excessive permissions: {0}{1}", apiResult.ExcessivePermissions.Any() ? string.Join(", ", apiResult.ExcessivePermissions.Order().Select(p => $"`{p}`")) : "none", Environment.NewLine)
143+
.AppendLine()
144+
.AppendLine("#### Requests")
145+
.AppendLine()
146+
.AppendJoin(Environment.NewLine, apiResult.Requests.Select(r => $"- {r}")).AppendLine()
147+
.AppendLine();
148+
}
149+
}
150+
else
151+
{
152+
sb.AppendLine("No APIs found.")
153+
.AppendLine();
154+
}
155+
156+
sb.AppendLine("## ⚠️ Unmatched requests")
157+
.AppendLine();
158+
159+
if (apiCenterMinimalPermissionsReport.UnmatchedRequests.Any())
160+
{
161+
sb.AppendLine("The following requests were not matched to any API in API Center:")
162+
.AppendLine()
163+
.AppendJoin(Environment.NewLine, apiCenterMinimalPermissionsReport.UnmatchedRequests
164+
.Select(r => $"- {r}").Order()).AppendLine()
165+
.AppendLine();
166+
}
167+
else
168+
{
169+
sb.AppendLine("No unmatched requests found.")
170+
.AppendLine();
171+
}
172+
173+
sb.AppendLine("## 🛑 Errors")
174+
.AppendLine();
175+
176+
if (apiCenterMinimalPermissionsReport.Errors.Any())
177+
{
178+
sb.AppendLine("The following errors occurred while determining minimal permissions:")
179+
.AppendLine()
180+
.AppendJoin(Environment.NewLine, apiCenterMinimalPermissionsReport.Errors
181+
.OrderBy(o => o.Request)
182+
.Select(e => $"- `{e.Request}`: {e.Error}")).AppendLine()
183+
.AppendLine();
184+
}
185+
else
186+
{
187+
sb.AppendLine("No errors occurred.");
188+
}
189+
190+
return sb.ToString();
191+
}
192+
106193
private static string? TransformApiCenterProductionVersionReport(object report)
107194
{
108195
var getReadableApiStatus = (ApiCenterProductionVersionPluginReportItemStatus status) => status switch
@@ -369,7 +456,7 @@ private static void AddExecutionSummaryReportSummary(IEnumerable<RequestLog> req
369456
}
370457

371458
sb.AppendLine();
372-
459+
373460
return sb.ToString();
374461
}
375462

dev-proxy-plugins/Reporters/PlainTextReporter.cs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ public class PlainTextReporter : BaseReporter
1616

1717
private readonly Dictionary<Type, Func<object, string?>> _transformers = new()
1818
{
19+
{ typeof(ApiCenterMinimalPermissionsPluginReport), TransformApiCenterMinimalPermissionsReport },
1920
{ typeof(ApiCenterOnboardingPluginReport), TransformApiCenterOnboardingReport },
2021
{ typeof(ApiCenterProductionVersionPluginReport), TransformApiCenterProductionVersionReport },
2122
{ typeof(ExecutionSummaryPluginReportByUrl), TransformExecutionSummaryByUrl },
@@ -217,6 +218,81 @@ private static void AddExecutionSummaryReportSummary(IEnumerable<RequestLog> req
217218
return sb.ToString();
218219
}
219220

221+
private static string? TransformApiCenterMinimalPermissionsReport(object report)
222+
{
223+
var apiCenterMinimalPermissionsReport = (ApiCenterMinimalPermissionsPluginReport)report;
224+
225+
var sb = new StringBuilder();
226+
227+
sb.AppendLine("Azure API Center minimal permissions report")
228+
.AppendLine();
229+
230+
sb.AppendLine("APIS")
231+
.AppendLine();
232+
233+
if (apiCenterMinimalPermissionsReport.Results.Any())
234+
{
235+
foreach (var apiResult in apiCenterMinimalPermissionsReport.Results)
236+
{
237+
sb.AppendFormat("{0}{1}", apiResult.ApiName, Environment.NewLine)
238+
.AppendLine()
239+
.AppendLine(apiResult.UsesMinimalPermissions ? "v Called using minimal permissions" : "x Called using excessive permissions")
240+
.AppendLine()
241+
.AppendLine("Permissions")
242+
.AppendLine()
243+
.AppendFormat("- Minimal permissions: {0}{1}", string.Join(", ", apiResult.MinimalPermissions.Order()), Environment.NewLine)
244+
.AppendFormat("- Permissions on the token: {0}{1}", string.Join(", ", apiResult.TokenPermissions.Order()), Environment.NewLine)
245+
.AppendFormat("- Excessive permissions: {0}{1}", apiResult.ExcessivePermissions.Any() ? string.Join(", ", apiResult.ExcessivePermissions.Order()) : "none", Environment.NewLine)
246+
.AppendLine()
247+
.AppendLine("Requests")
248+
.AppendLine()
249+
.AppendJoin(Environment.NewLine, apiResult.Requests.Select(r => $"- {r}")).AppendLine()
250+
.AppendLine();
251+
}
252+
}
253+
else
254+
{
255+
sb.AppendLine("No APIs found.")
256+
.AppendLine();
257+
}
258+
259+
sb.AppendLine("UNMATCHED REQUESTS")
260+
.AppendLine();
261+
262+
if (apiCenterMinimalPermissionsReport.UnmatchedRequests.Any())
263+
{
264+
sb.AppendLine("The following requests were not matched to any API in API Center:")
265+
.AppendLine()
266+
.AppendJoin(Environment.NewLine, apiCenterMinimalPermissionsReport.UnmatchedRequests
267+
.Select(r => $"- {r}").Order()).AppendLine()
268+
.AppendLine();
269+
}
270+
else
271+
{
272+
sb.AppendLine("No unmatched requests found.")
273+
.AppendLine();
274+
}
275+
276+
sb.AppendLine("ERRORS")
277+
.AppendLine();
278+
279+
if (apiCenterMinimalPermissionsReport.Errors.Any())
280+
{
281+
sb.AppendLine("The following errors occurred while determining minimal permissions:")
282+
.AppendLine()
283+
.AppendJoin(Environment.NewLine, apiCenterMinimalPermissionsReport.Errors
284+
.OrderBy(o => o.Request)
285+
.Select(e => $"- `{e.Request}`: {e.Error}")).AppendLine()
286+
.AppendLine();
287+
}
288+
else
289+
{
290+
sb.AppendLine("No errors occurred.");
291+
}
292+
293+
return sb.ToString();
294+
}
295+
220296
private static string? TransformApiCenterOnboardingReport(object report)
221297
{
222298
var apiCenterOnboardingReport = (ApiCenterOnboardingPluginReport)report;

0 commit comments

Comments
 (0)