Skip to content

Commit 5b20124

Browse files
Adds detecting minimal permissions. Closes #57 (#261)
* Adds detecting minimal permissions. Closes #57 * Updates the minimal permissions plugin to match the latest DevX API * Adds preview warning message * Adds fixes after code review
1 parent b0c5412 commit 5b20124

File tree

3 files changed

+395
-3
lines changed

3 files changed

+395
-3
lines changed

msgraph-developer-proxy-abstractions/ProxyUtils.cs

Lines changed: 172 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,32 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33

4+
using System.Text.RegularExpressions;
45
using Titanium.Web.Proxy.Http;
56
using Titanium.Web.Proxy.Models;
67

78
namespace Microsoft.Graph.DeveloperProxy.Abstractions;
89

10+
class ParsedSample {
11+
public string QueryVersion { get; set; } = string.Empty;
12+
public string RequestUrl { get; set; } = string.Empty;
13+
public string SampleUrl { get; set; } = string.Empty;
14+
public string Search { get; set; } = string.Empty;
15+
}
16+
917
public static class ProxyUtils {
10-
public static bool IsGraphRequest(Request request) =>
11-
request.RequestUri.Host.Contains("graph.microsoft.", StringComparison.OrdinalIgnoreCase) ||
12-
request.RequestUri.Host.Contains("microsoftgraph.", StringComparison.OrdinalIgnoreCase);
18+
private static readonly Regex itemPathRegex = new Regex(@"(?:\/)[\w]+:[\w\/.]+(:(?=\/)|$)");
19+
private static readonly Regex sanitizedItemPathRegex = new Regex("^[a-z]+:<value>$", RegexOptions.IgnoreCase);
20+
private static readonly Regex entityNameRegex = new Regex("^((microsoft.graph(.[a-z]+)+)|[a-z]+)$", RegexOptions.IgnoreCase);
21+
private static readonly Regex allAlphaRegex = new Regex("^[a-z]+$", RegexOptions.IgnoreCase);
22+
private static readonly Regex deprecationRegex = new Regex("^[a-z]+_v2$", RegexOptions.IgnoreCase);
23+
private static readonly Regex functionCallRegex = new Regex(@"^[a-z]+\(.*\)$", RegexOptions.IgnoreCase);
24+
25+
public static bool IsGraphRequest(Request request) => IsGraphUrl(request.RequestUri);
26+
27+
public static bool IsGraphUrl(Uri uri) =>
28+
uri.Host.StartsWith("graph.microsoft.", StringComparison.OrdinalIgnoreCase) ||
29+
uri.Host.StartsWith("microsoftgraph.", StringComparison.OrdinalIgnoreCase);
1330

1431
public static bool IsSdkRequest(Request request) => request.Headers.HeaderExists("SdkVersion");
1532

@@ -50,4 +67,156 @@ public static string ReplacePathTokens(string? path) {
5067
var appFolder = Path.GetDirectoryName(AppContext.BaseDirectory);
5168
return path.Replace("~appFolder", appFolder, StringComparison.OrdinalIgnoreCase);
5269
}
70+
71+
// from: https://github.com/microsoftgraph/microsoft-graph-explorer-v4/blob/db86b903f36ef1b882996d46aee52cd49ed4444b/src/app/utils/query-url-sanitization.ts
72+
public static string SanitizeUrl(string absoluteUrl) {
73+
absoluteUrl = Uri.UnescapeDataString(absoluteUrl);
74+
var uri = new Uri(absoluteUrl);
75+
76+
var parsedSample = ParseSampleUrl(absoluteUrl);
77+
var queryString = !String.IsNullOrEmpty(parsedSample.Search) ? $"?{SanitizeQueryParameters(parsedSample.Search)}" : "";
78+
79+
// Sanitize item path specified in query url
80+
var resourceUrl = parsedSample.RequestUrl;
81+
if (!String.IsNullOrEmpty(resourceUrl)) {
82+
resourceUrl = itemPathRegex.Replace(parsedSample.RequestUrl, match => {
83+
return $"{match.Value.Substring(0, match.Value.IndexOf(':'))}:<value>";
84+
});
85+
// Split requestUrl into segments that can be sanitized individually
86+
var urlSegments = resourceUrl.Split('/');
87+
for (var i = 0; i < urlSegments.Length; i++) {
88+
var segment = urlSegments[i];
89+
var sanitizedSegment = SanitizePathSegment(i < 1 ? "" : urlSegments[i - 1], segment);
90+
resourceUrl = resourceUrl.Replace(segment, sanitizedSegment);
91+
}
92+
}
93+
return $"{uri.GetLeftPart(UriPartial.Authority)}/{parsedSample.QueryVersion}/{resourceUrl}{queryString}";
94+
}
95+
96+
/**
97+
* Skipped segments:
98+
* - Entities, entity sets and navigation properties, expected to contain alphabetic letters only
99+
* - Deprecated entities in the form <entity>_v2
100+
* The remaining URL segments are assumed to be variables that need to be sanitized
101+
* @param segment
102+
*/
103+
private static string SanitizePathSegment(string previousSegment, string segment) {
104+
var segmentsToIgnore = new[] { "$value", "$count", "$ref", "$batch" };
105+
106+
if (IsAllAlpha(segment) ||
107+
IsDeprecation(segment) ||
108+
sanitizedItemPathRegex.IsMatch(segment) ||
109+
segmentsToIgnore.Contains(segment.ToLowerInvariant()) ||
110+
entityNameRegex.IsMatch(segment)) {
111+
return segment;
112+
}
113+
114+
// Check if segment is in this form: users('<some-id>|<UPN>') and transform to users(<value>)
115+
if (IsFunctionCall(segment)) {
116+
var openingBracketIndex = segment.IndexOf("(");
117+
var textWithinBrackets = segment.Substring(
118+
openingBracketIndex + 1,
119+
segment.Length - 2
120+
);
121+
var sanitizedText = String.Join(',', textWithinBrackets
122+
.Split(',')
123+
.Select(text => {
124+
if (text.Contains('=')) {
125+
var key = text.Split('=')[0];
126+
key = !IsAllAlpha(key) ? "<key>" : key;
127+
return $"{key}=<value>";
128+
}
129+
return "<value>";
130+
}));
131+
132+
return $"{segment.Substring(0, openingBracketIndex)}({sanitizedText})";
133+
}
134+
135+
if (IsPlaceHolderSegment(segment)) {
136+
return segment;
137+
}
138+
139+
if (!IsAllAlpha(previousSegment) && !IsDeprecation(previousSegment)) {
140+
previousSegment = "unknown";
141+
}
142+
143+
return $"{{{previousSegment}-id}}";
144+
}
145+
146+
private static string SanitizeQueryParameters(string queryString) {
147+
// remove leading ? from query string and decode
148+
queryString = Uri.UnescapeDataString(
149+
new Regex(@"\+").Replace(queryString.Substring(1), " ")
150+
);
151+
return String.Join('&', queryString.Split('&').Select(s => s));
152+
}
153+
154+
private static bool IsAllAlpha(string value) => allAlphaRegex.IsMatch(value);
155+
156+
private static bool IsDeprecation(string value) => deprecationRegex.IsMatch(value);
157+
158+
private static bool IsFunctionCall(string value) => functionCallRegex.IsMatch(value);
159+
160+
private static bool IsPlaceHolderSegment(string segment) {
161+
return segment.StartsWith('{') && segment.EndsWith('}');
162+
}
163+
164+
private static ParsedSample ParseSampleUrl(string url, string? version = null) {
165+
var parsedSample = new ParsedSample();
166+
167+
if (url != "") {
168+
try {
169+
url = RemoveExtraSlashesFromUrl(url);
170+
parsedSample.QueryVersion = version ?? GetGraphVersion(url);
171+
parsedSample.RequestUrl = GetRequestUrl(url, parsedSample.QueryVersion);
172+
parsedSample.Search = GenerateSearchParameters(url, "");
173+
parsedSample.SampleUrl = GenerateSampleUrl(url, parsedSample.QueryVersion, parsedSample.RequestUrl, parsedSample.Search);
174+
} catch (Exception) { }
175+
}
176+
177+
return parsedSample;
178+
}
179+
180+
private static string RemoveExtraSlashesFromUrl(string url) {
181+
return new Regex(@"([^:]\/)\/+").Replace(url, "$1");
182+
}
183+
184+
private static string GetGraphVersion(string url) {
185+
var uri = new Uri(url);
186+
return uri.Segments[1].Replace("/", "");
187+
}
188+
189+
private static string GetRequestUrl(string url, string version) {
190+
var uri = new Uri(url);
191+
var versionToReplace = uri.AbsolutePath.StartsWith($"/{version}")
192+
? version
193+
: GetGraphVersion(url);
194+
var requestContent = uri.AbsolutePath.Split(versionToReplace).LastOrDefault() ?? "";
195+
return Uri.UnescapeDataString(requestContent.TrimEnd('/')).TrimStart('/');
196+
}
197+
198+
private static string GenerateSearchParameters(string url, string search) {
199+
var uri = new Uri(url);
200+
201+
if (uri.Query != "") {
202+
try {
203+
search = Uri.UnescapeDataString(uri.Query);
204+
} catch (Exception) {
205+
search = uri.Query;
206+
}
207+
}
208+
209+
return new Regex(@"\s").Replace(search, "+");
210+
}
211+
212+
private static string GenerateSampleUrl(
213+
string url,
214+
string queryVersion,
215+
string requestUrl,
216+
string search
217+
) {
218+
var uri = new Uri(url);
219+
var origin = uri.GetLeftPart(UriPartial.Authority);
220+
return RemoveExtraSlashesFromUrl($"{origin}/{queryVersion}/{requestUrl + search}");
221+
}
53222
}

0 commit comments

Comments
 (0)