Skip to content

Commit 198d66c

Browse files
Add support for checking minimal permissions for any API without APIC. Closes #861 (#889)
1 parent 040ac45 commit 198d66c

20 files changed

+2397
-2087
lines changed

dev-proxy-plugins/GraphUtils.cs

Lines changed: 91 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -1,92 +1,92 @@
1-
// Copyright (c) Microsoft Corporation.
2-
// Licensed under the MIT License.
3-
4-
using System.Net.Http.Json;
5-
using Microsoft.DevProxy.Plugins.MinimalPermissions;
6-
using Microsoft.Extensions.Logging;
7-
using Titanium.Web.Proxy.Http;
8-
9-
namespace Microsoft.DevProxy.Plugins;
10-
11-
public class GraphUtils
12-
{
13-
// throttle requests per workload
14-
public static string BuildThrottleKey(Request r) => BuildThrottleKey(r.RequestUri);
15-
16-
public static string BuildThrottleKey(Uri uri)
17-
{
18-
if (uri.Segments.Length < 3)
19-
{
20-
return uri.Host;
21-
}
22-
23-
// first segment is /
24-
// second segment is Graph version (v1.0, beta)
25-
// third segment is the workload (users, groups, etc.)
26-
// segment can end with / if there are other segments following
27-
var workload = uri.Segments[2].Trim('/');
28-
29-
// TODO: handle 'me' which is a proxy to other resources
30-
31-
return workload;
32-
}
33-
34-
internal static string GetScopeTypeString(PermissionsType type)
35-
{
36-
return type switch
37-
{
38-
PermissionsType.Application => "Application",
39-
PermissionsType.Delegated => "DelegatedWork",
40-
_ => throw new InvalidOperationException($"Unknown scope type: {type}")
41-
};
42-
}
43-
44-
internal static async Task<IEnumerable<string>> UpdateUserScopesAsync(IEnumerable<string> minimalScopes, IEnumerable<(string method, string url)> endpoints, PermissionsType permissionsType, ILogger logger)
45-
{
46-
var userEndpoints = endpoints.Where(e => e.url.Contains("/users/{", StringComparison.OrdinalIgnoreCase));
47-
if (!userEndpoints.Any())
48-
{
49-
return minimalScopes;
50-
}
51-
52-
var newMinimalScopes = new HashSet<string>(minimalScopes);
53-
54-
var url = $"https://graphexplorerapi.azurewebsites.net/permissions?scopeType={GetScopeTypeString(permissionsType)}";
55-
using var httpClient = new HttpClient();
56-
var urls = userEndpoints.Select(e => {
57-
logger.LogDebug("Getting permissions for {method} {url}", e.method, e.url);
58-
return $"{url}&requesturl={e.url}&method={e.method}";
59-
});
60-
var tasks = urls.Select(u => {
61-
logger.LogTrace("Calling {url}...", u);
62-
return httpClient.GetFromJsonAsync<PermissionInfo[]>(u);
63-
});
64-
await Task.WhenAll(tasks);
65-
66-
foreach (var task in tasks)
67-
{
68-
var response = await task;
69-
if (response is null)
70-
{
71-
continue;
72-
}
73-
74-
// there's only one scope so it must be minimal already
75-
if (response.Length < 2)
76-
{
77-
continue;
78-
}
79-
80-
if (newMinimalScopes.Contains(response[0].Value))
81-
{
82-
logger.LogDebug("Replacing scope {old} with {new}", response[0].Value, response[1].Value);
83-
newMinimalScopes.Remove(response[0].Value);
84-
newMinimalScopes.Add(response[1].Value);
85-
}
86-
}
87-
88-
logger.LogDebug("Updated minimal scopes. Original: {original}, New: {new}", string.Join(", ", minimalScopes), string.Join(", ", newMinimalScopes));
89-
90-
return newMinimalScopes;
91-
}
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using System.Net.Http.Json;
5+
using Microsoft.DevProxy.Plugins.MinimalPermissions;
6+
using Microsoft.Extensions.Logging;
7+
using Titanium.Web.Proxy.Http;
8+
9+
namespace Microsoft.DevProxy.Plugins;
10+
11+
public class GraphUtils
12+
{
13+
// throttle requests per workload
14+
public static string BuildThrottleKey(Request r) => BuildThrottleKey(r.RequestUri);
15+
16+
public static string BuildThrottleKey(Uri uri)
17+
{
18+
if (uri.Segments.Length < 3)
19+
{
20+
return uri.Host;
21+
}
22+
23+
// first segment is /
24+
// second segment is Graph version (v1.0, beta)
25+
// third segment is the workload (users, groups, etc.)
26+
// segment can end with / if there are other segments following
27+
var workload = uri.Segments[2].Trim('/');
28+
29+
// TODO: handle 'me' which is a proxy to other resources
30+
31+
return workload;
32+
}
33+
34+
internal static string GetScopeTypeString(GraphPermissionsType type)
35+
{
36+
return type switch
37+
{
38+
GraphPermissionsType.Application => "Application",
39+
GraphPermissionsType.Delegated => "DelegatedWork",
40+
_ => throw new InvalidOperationException($"Unknown scope type: {type}")
41+
};
42+
}
43+
44+
internal static async Task<IEnumerable<string>> UpdateUserScopesAsync(IEnumerable<string> minimalScopes, IEnumerable<(string method, string url)> endpoints, GraphPermissionsType permissionsType, ILogger logger)
45+
{
46+
var userEndpoints = endpoints.Where(e => e.url.Contains("/users/{", StringComparison.OrdinalIgnoreCase));
47+
if (!userEndpoints.Any())
48+
{
49+
return minimalScopes;
50+
}
51+
52+
var newMinimalScopes = new HashSet<string>(minimalScopes);
53+
54+
var url = $"https://graphexplorerapi.azurewebsites.net/permissions?scopeType={GetScopeTypeString(permissionsType)}";
55+
using var httpClient = new HttpClient();
56+
var urls = userEndpoints.Select(e => {
57+
logger.LogDebug("Getting permissions for {method} {url}", e.method, e.url);
58+
return $"{url}&requesturl={e.url}&method={e.method}";
59+
});
60+
var tasks = urls.Select(u => {
61+
logger.LogTrace("Calling {url}...", u);
62+
return httpClient.GetFromJsonAsync<GraphPermissionInfo[]>(u);
63+
});
64+
await Task.WhenAll(tasks);
65+
66+
foreach (var task in tasks)
67+
{
68+
var response = await task;
69+
if (response is null)
70+
{
71+
continue;
72+
}
73+
74+
// there's only one scope so it must be minimal already
75+
if (response.Length < 2)
76+
{
77+
continue;
78+
}
79+
80+
if (newMinimalScopes.Contains(response[0].Value))
81+
{
82+
logger.LogDebug("Replacing scope {old} with {new}", response[0].Value, response[1].Value);
83+
newMinimalScopes.Remove(response[0].Value);
84+
newMinimalScopes.Add(response[1].Value);
85+
}
86+
}
87+
88+
logger.LogDebug("Updated minimal scopes. Original: {original}, New: {new}", string.Join(", ", minimalScopes), string.Join(", ", newMinimalScopes));
89+
90+
return newMinimalScopes;
91+
}
9292
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
namespace Microsoft.DevProxy.Plugins.MinimalPermissions;
5+
6+
public class ApiOperation
7+
{
8+
public required string Method { get; init; }
9+
public required string OriginalUrl { get; init; }
10+
public required string TokenizedUrl { get; init; }
11+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
namespace Microsoft.DevProxy.Plugins.MinimalPermissions;
5+
6+
public class ApiPermissionError
7+
{
8+
public required string Request { get; init; }
9+
public required string Error { get; init; }
10+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
namespace Microsoft.DevProxy.Plugins.MinimalPermissions;
5+
6+
public class ApiPermissionsInfo
7+
{
8+
public required List<string> TokenPermissions { get; init; }
9+
public required List<ApiOperation> OperationsFromRequests { get; init; }
10+
public required string[] MinimalScopes { get; init; }
11+
public required string[] UnmatchedOperations { get; init; }
12+
public required List<ApiPermissionError> Errors { get; init; }
13+
}
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1-
using System.Text.Json.Serialization;
2-
3-
namespace Microsoft.DevProxy.Plugins.MinimalPermissions;
4-
5-
internal class PermissionError
6-
{
7-
[JsonPropertyName("requestUrl")]
8-
public string Url { get; set; } = string.Empty;
9-
public string Message { get; set; } = string.Empty;
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using System.Text.Json.Serialization;
5+
6+
namespace Microsoft.DevProxy.Plugins.MinimalPermissions;
7+
8+
internal class GraphPermissionError
9+
{
10+
[JsonPropertyName("requestUrl")]
11+
public string Url { get; set; } = string.Empty;
12+
public string Message { get; set; } = string.Empty;
1013
}
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
1-
namespace Microsoft.DevProxy.Plugins.MinimalPermissions;
2-
3-
internal class PermissionInfo
4-
{
5-
public string Value { get; set; } = string.Empty;
6-
public string ScopeType { get; set; } = string.Empty;
7-
public string ConsentDisplayName { get; set; } = string.Empty;
8-
public string ConsentDescription { get; set; } = string.Empty;
9-
public bool IsAdmin { get; set; }
10-
public bool IsLeastPrivilege { get; set; }
11-
public bool IsHidden { get; set; }
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
namespace Microsoft.DevProxy.Plugins.MinimalPermissions;
5+
6+
internal class GraphPermissionInfo
7+
{
8+
public string Value { get; set; } = string.Empty;
9+
public string ScopeType { get; set; } = string.Empty;
10+
public string ConsentDisplayName { get; set; } = string.Empty;
11+
public string ConsentDescription { get; set; } = string.Empty;
12+
public bool IsAdmin { get; set; }
13+
public bool IsLeastPrivilege { get; set; }
14+
public bool IsHidden { get; set; }
1215
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
namespace Microsoft.DevProxy.Plugins.MinimalPermissions;
5+
6+
public enum GraphPermissionsType
7+
{
8+
Application,
9+
Delegated
10+
}
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
1-
2-
using System.Text.Json.Serialization;
3-
4-
namespace Microsoft.DevProxy.Plugins.MinimalPermissions;
5-
6-
public class RequestInfo
7-
{
8-
[JsonPropertyName("requestUrl")]
9-
public string Url { get; set; } = string.Empty;
10-
public string Method { get; set; } = string.Empty;
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
5+
using System.Text.Json.Serialization;
6+
7+
namespace Microsoft.DevProxy.Plugins.MinimalPermissions;
8+
9+
public class GraphRequestInfo
10+
{
11+
[JsonPropertyName("requestUrl")]
12+
public string Url { get; set; } = string.Empty;
13+
public string Method { get; set; } = string.Empty;
1114
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
namespace Microsoft.DevProxy.Plugins.MinimalPermissions;
5+
6+
internal class GraphResultsAndErrors
7+
{
8+
public GraphPermissionInfo[]? Results { get; set; }
9+
public GraphPermissionError[]? Errors { get; set; }
10+
}

0 commit comments

Comments
 (0)