Skip to content

Adds the ApiCenterMinimalPermissionsPlugin #761

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

Merged
Merged
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
24 changes: 24 additions & 0 deletions dev-proxy-plugins/ApiCenter/ModelExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,12 @@ internal static async Task<Dictionary<string, ApiDefinition>> GetApiDefinitionsB
}
}

logger.LogDebug(
"Loaded API definitions from API Center for APIs:{newLine}- {apis}",
Environment.NewLine,
string.Join($"{Environment.NewLine}- ", apiDefinitions.Keys)
);

return apiDefinitions;
}

Expand All @@ -259,4 +265,22 @@ internal static async Task<Dictionary<string, ApiDefinition>> GetApiDefinitionsB
return api.Api;
}
}

internal static Api? FindApiByDefinition(this Api[] apis, ApiDefinition apiDefinition, ILogger logger)
{
var api = apis
.FirstOrDefault(a =>
(a.Versions?.Any(v => v.Definitions?.Any(d => d.Id == apiDefinition.Id) == true) == true) ||
(a.Deployments?.Any(d => d.Properties?.DefinitionId == apiDefinition.Id) == true));
if (api is null)
{
logger.LogDebug("No matching API found for {apiDefinitionId}", apiDefinition.Id);
return null;
}
else
{
logger.LogDebug("API {api} found for {apiDefinitionId}", api.Name, apiDefinition.Id);
return api;
}
}
}
49 changes: 45 additions & 4 deletions dev-proxy-plugins/OpenApi/OpenApiDocumentExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ namespace Microsoft.OpenApi.Models;

public static class OpenApiDocumentExtensions
{
public static OpenApiPathItem? FindMatchingPathItem(this OpenApiDocument openApiDocument, string requestUrl, ILogger logger)
public static KeyValuePair<string, OpenApiPathItem>? FindMatchingPathItem(this OpenApiDocument openApiDocument, string requestUrl, ILogger logger)
{
foreach (var server in openApiDocument.Servers)
{
Expand Down Expand Up @@ -50,7 +50,7 @@ public static class OpenApiDocumentExtensions
{
logger.LogDebug("Regex matches {requestUrl}", urlPathFromRequest);

return path.Value;
return path;
}

logger.LogDebug("Regex does not match {requestUrl}", urlPathFromRequest);
Expand All @@ -60,8 +60,7 @@ public static class OpenApiDocumentExtensions
if (urlPathFromRequest.Equals(urlPathFromSpec, StringComparison.OrdinalIgnoreCase))
{
logger.LogDebug("{requestUrl} matches {urlPath}", requestUrl, urlPathFromSpec);

return path.Value;
return path;
}

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

return null;
}

public static string[] GetEffectiveScopes(this OpenApiOperation operation, OpenApiDocument openApiDocument, ILogger logger)
{
var oauth2Scheme = openApiDocument.GetOAuth2Schemes().FirstOrDefault();
if (oauth2Scheme is null)
{
logger.LogDebug("No OAuth2 schemes found in OpenAPI document");
return [];
}

var globalScopes = new string[] { };
var globalOAuth2Requirement = openApiDocument.SecurityRequirements
.FirstOrDefault(req => req.ContainsKey(oauth2Scheme));
if (globalOAuth2Requirement is not null)
{
globalScopes = [.. globalOAuth2Requirement[oauth2Scheme]];
}

if (operation.Security is null)
{
logger.LogDebug("No security requirements found in operation {operation}", operation.OperationId);
return globalScopes;
}

var operationOAuth2Requirement = operation.Security
.Where(req => req.ContainsKey(oauth2Scheme))
.SelectMany(req => req[oauth2Scheme]);
if (operationOAuth2Requirement is not null)
{
return operationOAuth2Requirement.ToArray();
}

return [];
}

public static OpenApiSecurityScheme[] GetOAuth2Schemes(this OpenApiDocument openApiDocument)
{
return openApiDocument.Components.SecuritySchemes
.Where(s => s.Value.Type == SecuritySchemeType.OAuth2)
.Select(s => s.Value)
.ToArray();
}
}
89 changes: 88 additions & 1 deletion dev-proxy-plugins/Reporters/MarkdownReporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ public class MarkdownReporter : BaseReporter

private readonly Dictionary<Type, Func<object, string?>> _transformers = new()
{
{ typeof(ApiCenterMinimalPermissionsPluginReport), TransformApiCenterMinimalPermissionsReport },
{ typeof(ApiCenterOnboardingPluginReport), TransformApiCenterOnboardingReport },
{ typeof(ApiCenterProductionVersionPluginReport), TransformApiCenterProductionVersionReport },
{ typeof(ExecutionSummaryPluginReportByUrl), TransformExecutionSummaryByUrl },
Expand Down Expand Up @@ -103,6 +104,92 @@ public MarkdownReporter(IPluginEvents pluginEvents, IProxyContext context, ILogg
return sb.ToString();
}

private static string? TransformApiCenterMinimalPermissionsReport(object report)
{
var apiCenterMinimalPermissionsReport = (ApiCenterMinimalPermissionsPluginReport)report;

var sb = new StringBuilder();
sb.AppendLine("# Azure API Center minimal permissions report")
.AppendLine();

sb.AppendLine("## ℹ️ Summary")
.AppendLine()
.AppendLine("<table>")
.AppendFormat("<tr><td>🔎 APIs inspected</td><td align=\"right\">{0}</td></tr>{1}", apiCenterMinimalPermissionsReport.Results.Length, Environment.NewLine)
.AppendFormat("<tr><td>🔎 Requests inspected</td><td align=\"right\">{0}</td></tr>{1}", apiCenterMinimalPermissionsReport.Results.Sum(r => r.Requests.Length), Environment.NewLine)
.AppendFormat("<tr><td>✅ APIs called using minimal permissions</td><td align=\"right\">{0}</td></tr>{1}", apiCenterMinimalPermissionsReport.Results.Count(r => r.UsesMinimalPermissions), Environment.NewLine)
.AppendFormat("<tr><td>🛑 APIs called using excessive permissions</td><td align=\"right\">{0}</td></tr>{1}", apiCenterMinimalPermissionsReport.Results.Count(r => !r.UsesMinimalPermissions), Environment.NewLine)
.AppendFormat("<tr><td>⚠️ Unmatched requests</td><td align=\"right\">{0}</td></tr>{1}", apiCenterMinimalPermissionsReport.UnmatchedRequests.Length, Environment.NewLine)
.AppendFormat("<tr><td>🛑 Errors</td><td align=\"right\">{0}</td></tr>{1}", apiCenterMinimalPermissionsReport.Errors.Length, Environment.NewLine)
.AppendLine("</table>")
.AppendLine();

sb.AppendLine("## 🔌 APIs")
.AppendLine();

if (apiCenterMinimalPermissionsReport.Results.Any())
{
foreach (var apiResult in apiCenterMinimalPermissionsReport.Results)
{
sb.AppendFormat("### {0}{1}", apiResult.ApiName, Environment.NewLine)
.AppendLine()
.AppendFormat(apiResult.UsesMinimalPermissions ? "✅ Called using minimal permissions{0}" : "🛑 Called using excessive permissions{0}", Environment.NewLine)
.AppendLine()
.AppendLine("#### Permissions")
.AppendLine()
.AppendFormat("- Minimal permissions: {0}{1}", string.Join(", ", apiResult.MinimalPermissions.Order().Select(p => $"`{p}`")), Environment.NewLine)
.AppendFormat("- Permissions on the token: {0}{1}", string.Join(", ", apiResult.TokenPermissions.Order().Select(p => $"`{p}`")), Environment.NewLine)
.AppendFormat("- Excessive permissions: {0}{1}", apiResult.ExcessivePermissions.Any() ? string.Join(", ", apiResult.ExcessivePermissions.Order().Select(p => $"`{p}`")) : "none", Environment.NewLine)
.AppendLine()
.AppendLine("#### Requests")
.AppendLine()
.AppendJoin(Environment.NewLine, apiResult.Requests.Select(r => $"- {r}")).AppendLine()
.AppendLine();
}
}
else
{
sb.AppendLine("No APIs found.")
.AppendLine();
}

sb.AppendLine("## ⚠️ Unmatched requests")
.AppendLine();

if (apiCenterMinimalPermissionsReport.UnmatchedRequests.Any())
{
sb.AppendLine("The following requests were not matched to any API in API Center:")
.AppendLine()
.AppendJoin(Environment.NewLine, apiCenterMinimalPermissionsReport.UnmatchedRequests
.Select(r => $"- {r}").Order()).AppendLine()
.AppendLine();
}
else
{
sb.AppendLine("No unmatched requests found.")
.AppendLine();
}

sb.AppendLine("## 🛑 Errors")
.AppendLine();

if (apiCenterMinimalPermissionsReport.Errors.Any())
{
sb.AppendLine("The following errors occurred while determining minimal permissions:")
.AppendLine()
.AppendJoin(Environment.NewLine, apiCenterMinimalPermissionsReport.Errors
.OrderBy(o => o.Request)
.Select(e => $"- `{e.Request}`: {e.Error}")).AppendLine()
.AppendLine();
}
else
{
sb.AppendLine("No errors occurred.");
}

return sb.ToString();
}

private static string? TransformApiCenterProductionVersionReport(object report)
{
var getReadableApiStatus = (ApiCenterProductionVersionPluginReportItemStatus status) => status switch
Expand Down Expand Up @@ -369,7 +456,7 @@ private static void AddExecutionSummaryReportSummary(IEnumerable<RequestLog> req
}

sb.AppendLine();

return sb.ToString();
}

Expand Down
76 changes: 76 additions & 0 deletions dev-proxy-plugins/Reporters/PlainTextReporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ public class PlainTextReporter : BaseReporter

private readonly Dictionary<Type, Func<object, string?>> _transformers = new()
{
{ typeof(ApiCenterMinimalPermissionsPluginReport), TransformApiCenterMinimalPermissionsReport },
{ typeof(ApiCenterOnboardingPluginReport), TransformApiCenterOnboardingReport },
{ typeof(ApiCenterProductionVersionPluginReport), TransformApiCenterProductionVersionReport },
{ typeof(ExecutionSummaryPluginReportByUrl), TransformExecutionSummaryByUrl },
Expand Down Expand Up @@ -217,6 +218,81 @@ private static void AddExecutionSummaryReportSummary(IEnumerable<RequestLog> req
return sb.ToString();
}

private static string? TransformApiCenterMinimalPermissionsReport(object report)
{
var apiCenterMinimalPermissionsReport = (ApiCenterMinimalPermissionsPluginReport)report;

var sb = new StringBuilder();

sb.AppendLine("Azure API Center minimal permissions report")
.AppendLine();

sb.AppendLine("APIS")
.AppendLine();

if (apiCenterMinimalPermissionsReport.Results.Any())
{
foreach (var apiResult in apiCenterMinimalPermissionsReport.Results)
{
sb.AppendFormat("{0}{1}", apiResult.ApiName, Environment.NewLine)
.AppendLine()
.AppendLine(apiResult.UsesMinimalPermissions ? "v Called using minimal permissions" : "x Called using excessive permissions")
.AppendLine()
.AppendLine("Permissions")
.AppendLine()
.AppendFormat("- Minimal permissions: {0}{1}", string.Join(", ", apiResult.MinimalPermissions.Order()), Environment.NewLine)
.AppendFormat("- Permissions on the token: {0}{1}", string.Join(", ", apiResult.TokenPermissions.Order()), Environment.NewLine)
.AppendFormat("- Excessive permissions: {0}{1}", apiResult.ExcessivePermissions.Any() ? string.Join(", ", apiResult.ExcessivePermissions.Order()) : "none", Environment.NewLine)
.AppendLine()
.AppendLine("Requests")
.AppendLine()
.AppendJoin(Environment.NewLine, apiResult.Requests.Select(r => $"- {r}")).AppendLine()
.AppendLine();
}
}
else
{
sb.AppendLine("No APIs found.")
.AppendLine();
}

sb.AppendLine("UNMATCHED REQUESTS")
.AppendLine();

if (apiCenterMinimalPermissionsReport.UnmatchedRequests.Any())
{
sb.AppendLine("The following requests were not matched to any API in API Center:")
.AppendLine()
.AppendJoin(Environment.NewLine, apiCenterMinimalPermissionsReport.UnmatchedRequests
.Select(r => $"- {r}").Order()).AppendLine()
.AppendLine();
}
else
{
sb.AppendLine("No unmatched requests found.")
.AppendLine();
}

sb.AppendLine("ERRORS")
.AppendLine();

if (apiCenterMinimalPermissionsReport.Errors.Any())
{
sb.AppendLine("The following errors occurred while determining minimal permissions:")
.AppendLine()
.AppendJoin(Environment.NewLine, apiCenterMinimalPermissionsReport.Errors
.OrderBy(o => o.Request)
.Select(e => $"- `{e.Request}`: {e.Error}")).AppendLine()
.AppendLine();
}
else
{
sb.AppendLine("No errors occurred.");
}

return sb.ToString();
}

private static string? TransformApiCenterOnboardingReport(object report)
{
var apiCenterOnboardingReport = (ApiCenterOnboardingPluginReport)report;
Expand Down
Loading