Skip to content

Commit 6dc58e7

Browse files
Extends minimal permissions plugins with support for batch requests. Closes #283 (#293)
1 parent d568850 commit 6dc58e7

File tree

5 files changed

+135
-9
lines changed

5 files changed

+135
-9
lines changed
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using System.Text.Json.Serialization;
5+
6+
namespace Microsoft365.DeveloperProxy.Abstractions;
7+
8+
public class GraphBatchRequestPayload {
9+
[JsonPropertyName("requests")]
10+
public GraphBatchRequestPayloadRequest[] Requests { get; set; } = Array.Empty<GraphBatchRequestPayloadRequest>();
11+
}
12+
13+
public class GraphBatchRequestPayloadRequest {
14+
[JsonPropertyName("id")]
15+
public string Id { get; set; } = string.Empty;
16+
[JsonPropertyName("method")]
17+
public string Method { get; set; } = string.Empty;
18+
[JsonPropertyName("url")]
19+
public string Url { get; set; } = string.Empty;
20+
[JsonPropertyName("headers")]
21+
public Dictionary<string, string>? Headers { get; set; } = new Dictionary<string, string>();
22+
[JsonPropertyName("body")]
23+
public object? Body { get; set; }
24+
}

m365-developer-proxy-abstractions/ProxyUtils.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,17 @@ public static bool IsGraphUrl(Uri uri) =>
2828
uri.Host.StartsWith("graph.microsoft.", StringComparison.OrdinalIgnoreCase) ||
2929
uri.Host.StartsWith("microsoftgraph.", StringComparison.OrdinalIgnoreCase);
3030

31+
public static bool IsGraphBatchUrl(Uri uri) =>
32+
uri.AbsoluteUri.EndsWith("/$batch", StringComparison.OrdinalIgnoreCase);
33+
3134
public static bool IsSdkRequest(Request request) => request.Headers.HeaderExists("SdkVersion");
3235

3336
public static bool IsGraphBetaRequest(Request request) =>
3437
IsGraphRequest(request) &&
35-
request.RequestUri.AbsolutePath.Contains("/beta/", StringComparison.OrdinalIgnoreCase);
38+
IsGraphBetaUrl(request.RequestUri);
39+
40+
public static bool IsGraphBetaUrl(Uri uri) =>
41+
uri.AbsolutePath.Contains("/beta/", StringComparison.OrdinalIgnoreCase);
3642

3743
/// <summary>
3844
/// Utility to build HTTP response headers consistent with Microsoft Graph

m365-developer-proxy-plugins/RequestLogs/MinimalPermissionsGuidancePlugin.cs

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,21 +48,34 @@ private async void AfterRecordingStop(object? sender, RecordingArgs e)
4848

4949
var methodAndUrlString = request.Message.First();
5050
var methodAndUrl = GetMethodAndUrl(methodAndUrlString);
51+
var requestsFromBatch = Array.Empty<Tuple<string, string>>();
5152

52-
if (!ProxyUtils.IsGraphUrl(new Uri(methodAndUrl.Item2)))
53+
var uri = new Uri(methodAndUrl.Item2);
54+
if (!ProxyUtils.IsGraphUrl(uri))
5355
{
5456
continue;
5557
}
5658

57-
methodAndUrl = new Tuple<string, string>(methodAndUrl.Item1, GetTokenizedUrl(methodAndUrl.Item2));
59+
if (ProxyUtils.IsGraphBatchUrl(uri)) {
60+
var graphVersion = ProxyUtils.IsGraphBetaUrl(uri) ? "beta" : "v1.0";
61+
requestsFromBatch = GetRequestsFromBatch(request.Context?.Session.HttpClient.Request.BodyString!, graphVersion, uri.Host);
62+
}
63+
else {
64+
methodAndUrl = new Tuple<string, string>(methodAndUrl.Item1, GetTokenizedUrl(methodAndUrl.Item2));
65+
}
5866

5967
var scopesAndType = GetPermissionsAndType(request);
6068
if (scopesAndType.Item1 == PermissionsType.Delegated)
6169
{
6270
// use the scopes from the last request in case the app is using incremental consent
6371
scopesToEvaluate = scopesAndType.Item2;
6472

65-
delegatedEndpoints.Add(methodAndUrl);
73+
if (ProxyUtils.IsGraphBatchUrl(uri)) {
74+
delegatedEndpoints.AddRange(requestsFromBatch);
75+
}
76+
else {
77+
delegatedEndpoints.Add(methodAndUrl);
78+
}
6679
}
6780
else
6881
{
@@ -74,7 +87,12 @@ private async void AfterRecordingStop(object? sender, RecordingArgs e)
7487
rolesToEvaluate.Length == 0) {
7588
rolesToEvaluate = scopesAndType.Item2;
7689

77-
applicationEndpoints.Add(methodAndUrl);
90+
if (ProxyUtils.IsGraphBatchUrl(uri)) {
91+
applicationEndpoints.AddRange(requestsFromBatch);
92+
}
93+
else {
94+
applicationEndpoints.Add(methodAndUrl);
95+
}
7896
}
7997
}
8098
}
@@ -112,6 +130,38 @@ private async void AfterRecordingStop(object? sender, RecordingArgs e)
112130
}
113131
}
114132

133+
private Tuple<string, string>[] GetRequestsFromBatch(string batchBody, string graphVersion, string graphHostName)
134+
{
135+
var requests = new List<Tuple<string, string>>();
136+
137+
if (String.IsNullOrEmpty(batchBody))
138+
{
139+
return requests.ToArray();
140+
}
141+
142+
try {
143+
var batch = JsonSerializer.Deserialize<GraphBatchRequestPayload>(batchBody);
144+
if (batch == null)
145+
{
146+
return requests.ToArray();
147+
}
148+
149+
foreach (var request in batch.Requests)
150+
{
151+
try {
152+
var method = request.Method;
153+
var url = request.Url;
154+
var absoluteUrl = $"https://{graphHostName}/{graphVersion}{url}";
155+
requests.Add(new Tuple<string, string>(method, GetTokenizedUrl(absoluteUrl)));
156+
}
157+
catch {}
158+
}
159+
}
160+
catch {}
161+
162+
return requests.ToArray();
163+
}
164+
115165
/// <summary>
116166
/// Returns permissions and type (delegated or application) from the access token
117167
/// used on the request.

m365-developer-proxy-plugins/RequestLogs/MinimalPermissionsPlugin.cs

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,14 +52,21 @@ private async void AfterRecordingStop(object? sender, RecordingArgs e)
5252
var methodAndUrlString = request.Message.First();
5353
var methodAndUrl = GetMethodAndUrl(methodAndUrlString);
5454

55-
if (!ProxyUtils.IsGraphUrl(new Uri(methodAndUrl.Item2)))
55+
var uri = new Uri(methodAndUrl.Item2);
56+
if (!ProxyUtils.IsGraphUrl(uri))
5657
{
5758
continue;
5859
}
5960

60-
methodAndUrl = new Tuple<string, string>(methodAndUrl.Item1, GetTokenizedUrl(methodAndUrl.Item2));
61-
62-
endpoints.Add(methodAndUrl);
61+
if (ProxyUtils.IsGraphBatchUrl(uri)) {
62+
var graphVersion = ProxyUtils.IsGraphBetaUrl(uri) ? "beta" : "v1.0";
63+
var requestsFromBatch = GetRequestsFromBatch(request.Context?.Session.HttpClient.Request.BodyString!, graphVersion, uri.Host);
64+
endpoints.AddRange(requestsFromBatch);
65+
}
66+
else {
67+
methodAndUrl = new Tuple<string, string>(methodAndUrl.Item1, GetTokenizedUrl(methodAndUrl.Item2));
68+
endpoints.Add(methodAndUrl);
69+
}
6370
}
6471

6572
// Remove duplicates
@@ -82,6 +89,38 @@ private async void AfterRecordingStop(object? sender, RecordingArgs e)
8289
await DetermineMinimalScopes(endpoints);
8390
}
8491

92+
private Tuple<string, string>[] GetRequestsFromBatch(string batchBody, string graphVersion, string graphHostName)
93+
{
94+
var requests = new List<Tuple<string, string>>();
95+
96+
if (String.IsNullOrEmpty(batchBody))
97+
{
98+
return requests.ToArray();
99+
}
100+
101+
try {
102+
var batch = JsonSerializer.Deserialize<GraphBatchRequestPayload>(batchBody);
103+
if (batch == null)
104+
{
105+
return requests.ToArray();
106+
}
107+
108+
foreach (var request in batch.Requests)
109+
{
110+
try {
111+
var method = request.Method;
112+
var url = request.Url;
113+
var absoluteUrl = $"https://{graphHostName}/{graphVersion}{url}";
114+
requests.Add(new Tuple<string, string>(method, GetTokenizedUrl(absoluteUrl)));
115+
}
116+
catch {}
117+
}
118+
}
119+
catch {}
120+
121+
return requests.ToArray();
122+
}
123+
85124
private string GetScopeTypeString()
86125
{
87126
return _configuration.Type switch

m365-developer-proxy/ProxyEngine.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,13 @@ async Task OnRequest(object sender, SessionEventArgs e) {
340340
var method = e.HttpClient.Request.Method.ToUpper();
341341
// The proxy does not intercept or alter OPTIONS requests
342342
if (method is not "OPTIONS" && IsProxiedHost(e.HttpClient.Request.RequestUri.Host)) {
343+
// // we need to keep the request body for further processing
344+
// // by plugins
345+
e.HttpClient.Request.KeepBody = true;
346+
if (e.HttpClient.Request.HasBody) {
347+
await e.GetRequestBodyAsString();
348+
}
349+
343350
e.UserData = e.HttpClient.Request;
344351
_logger.LogRequest(new[] { $"{e.HttpClient.Request.Method} {e.HttpClient.Request.Url}" }, MessageType.InterceptedRequest, new LoggingContext(e));
345352
HandleRequest(e);

0 commit comments

Comments
 (0)