Skip to content

Commit

Permalink
Add support for checking minimal permissions for any API without APIC.
Browse files Browse the repository at this point in the history
…Closes #861 (#889)
  • Loading branch information
waldekmastykarz authored Oct 3, 2024
1 parent 040ac45 commit 198d66c
Show file tree
Hide file tree
Showing 20 changed files with 2,397 additions and 2,087 deletions.
182 changes: 91 additions & 91 deletions dev-proxy-plugins/GraphUtils.cs
Original file line number Diff line number Diff line change
@@ -1,92 +1,92 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Net.Http.Json;
using Microsoft.DevProxy.Plugins.MinimalPermissions;
using Microsoft.Extensions.Logging;
using Titanium.Web.Proxy.Http;

namespace Microsoft.DevProxy.Plugins;

public class GraphUtils
{
// throttle requests per workload
public static string BuildThrottleKey(Request r) => BuildThrottleKey(r.RequestUri);

public static string BuildThrottleKey(Uri uri)
{
if (uri.Segments.Length < 3)
{
return uri.Host;
}

// first segment is /
// second segment is Graph version (v1.0, beta)
// third segment is the workload (users, groups, etc.)
// segment can end with / if there are other segments following
var workload = uri.Segments[2].Trim('/');

// TODO: handle 'me' which is a proxy to other resources

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<IEnumerable<string>> UpdateUserScopesAsync(IEnumerable<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<string>(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<PermissionInfo[]>(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;
}
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Net.Http.Json;
using Microsoft.DevProxy.Plugins.MinimalPermissions;
using Microsoft.Extensions.Logging;
using Titanium.Web.Proxy.Http;

namespace Microsoft.DevProxy.Plugins;

public class GraphUtils
{
// throttle requests per workload
public static string BuildThrottleKey(Request r) => BuildThrottleKey(r.RequestUri);

public static string BuildThrottleKey(Uri uri)
{
if (uri.Segments.Length < 3)
{
return uri.Host;
}

// first segment is /
// second segment is Graph version (v1.0, beta)
// third segment is the workload (users, groups, etc.)
// segment can end with / if there are other segments following
var workload = uri.Segments[2].Trim('/');

// TODO: handle 'me' which is a proxy to other resources

return workload;
}

internal static string GetScopeTypeString(GraphPermissionsType type)
{
return type switch
{
GraphPermissionsType.Application => "Application",
GraphPermissionsType.Delegated => "DelegatedWork",
_ => throw new InvalidOperationException($"Unknown scope type: {type}")
};
}

internal static async Task<IEnumerable<string>> UpdateUserScopesAsync(IEnumerable<string> minimalScopes, IEnumerable<(string method, string url)> endpoints, GraphPermissionsType permissionsType, ILogger logger)
{
var userEndpoints = endpoints.Where(e => e.url.Contains("/users/{", StringComparison.OrdinalIgnoreCase));
if (!userEndpoints.Any())
{
return minimalScopes;
}

var newMinimalScopes = new HashSet<string>(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<GraphPermissionInfo[]>(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;
}
}
11 changes: 11 additions & 0 deletions dev-proxy-plugins/MinimalPermissions/ApiOperation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace Microsoft.DevProxy.Plugins.MinimalPermissions;

public class ApiOperation
{
public required string Method { get; init; }
public required string OriginalUrl { get; init; }
public required string TokenizedUrl { get; init; }
}
10 changes: 10 additions & 0 deletions dev-proxy-plugins/MinimalPermissions/ApiPermissionError.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace Microsoft.DevProxy.Plugins.MinimalPermissions;

public class ApiPermissionError
{
public required string Request { get; init; }
public required string Error { get; init; }
}
13 changes: 13 additions & 0 deletions dev-proxy-plugins/MinimalPermissions/ApiPermissionsInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace Microsoft.DevProxy.Plugins.MinimalPermissions;

public class ApiPermissionsInfo
{
public required List<string> TokenPermissions { get; init; }
public required List<ApiOperation> OperationsFromRequests { get; init; }
public required string[] MinimalScopes { get; init; }
public required string[] UnmatchedOperations { get; init; }
public required List<ApiPermissionError> Errors { get; init; }
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
using System.Text.Json.Serialization;

namespace Microsoft.DevProxy.Plugins.MinimalPermissions;

internal class PermissionError
{
[JsonPropertyName("requestUrl")]
public string Url { get; set; } = string.Empty;
public string Message { get; set; } = string.Empty;
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Text.Json.Serialization;

namespace Microsoft.DevProxy.Plugins.MinimalPermissions;

internal class GraphPermissionError
{
[JsonPropertyName("requestUrl")]
public string Url { get; set; } = string.Empty;
public string Message { get; set; } = string.Empty;
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
namespace Microsoft.DevProxy.Plugins.MinimalPermissions;

internal class PermissionInfo
{
public string Value { get; set; } = string.Empty;
public string ScopeType { get; set; } = string.Empty;
public string ConsentDisplayName { get; set; } = string.Empty;
public string ConsentDescription { get; set; } = string.Empty;
public bool IsAdmin { get; set; }
public bool IsLeastPrivilege { get; set; }
public bool IsHidden { get; set; }
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace Microsoft.DevProxy.Plugins.MinimalPermissions;

internal class GraphPermissionInfo
{
public string Value { get; set; } = string.Empty;
public string ScopeType { get; set; } = string.Empty;
public string ConsentDisplayName { get; set; } = string.Empty;
public string ConsentDescription { get; set; } = string.Empty;
public bool IsAdmin { get; set; }
public bool IsLeastPrivilege { get; set; }
public bool IsHidden { get; set; }
}
10 changes: 10 additions & 0 deletions dev-proxy-plugins/MinimalPermissions/GraphPermissionsType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace Microsoft.DevProxy.Plugins.MinimalPermissions;

public enum GraphPermissionsType
{
Application,
Delegated
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@

using System.Text.Json.Serialization;

namespace Microsoft.DevProxy.Plugins.MinimalPermissions;

public class RequestInfo
{
[JsonPropertyName("requestUrl")]
public string Url { get; set; } = string.Empty;
public string Method { get; set; } = string.Empty;
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.


using System.Text.Json.Serialization;

namespace Microsoft.DevProxy.Plugins.MinimalPermissions;

public class GraphRequestInfo
{
[JsonPropertyName("requestUrl")]
public string Url { get; set; } = string.Empty;
public string Method { get; set; } = string.Empty;
}
10 changes: 10 additions & 0 deletions dev-proxy-plugins/MinimalPermissions/GraphResultsAndErrors.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace Microsoft.DevProxy.Plugins.MinimalPermissions;

internal class GraphResultsAndErrors
{
public GraphPermissionInfo[]? Results { get; set; }
public GraphPermissionError[]? Errors { get; set; }
}
Loading

0 comments on commit 198d66c

Please sign in to comment.