Skip to content

Commit a603656

Browse files
Adds Minimal SharePoint CSOM permissions plugin. Closes #1018 (#1061)
1 parent 7c2ca67 commit a603656

File tree

7 files changed

+560
-0
lines changed

7 files changed

+560
-0
lines changed
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System.Text.Json;
6+
using System.Text.Json.Serialization;
7+
using System.Xml;
8+
9+
namespace DevProxy.Plugins.MinimalPermissions;
10+
11+
enum ActionType
12+
{
13+
ObjectPath,
14+
Query,
15+
SetProperty
16+
}
17+
18+
enum AccessType
19+
{
20+
Delegated,
21+
Application
22+
}
23+
24+
static class CsomParser
25+
{
26+
private static readonly JsonSerializerOptions options = new()
27+
{
28+
PropertyNameCaseInsensitive = true,
29+
Converters =
30+
{
31+
new JsonStringEnumConverter(JsonNamingPolicy.CamelCase)
32+
}
33+
};
34+
35+
private static string? GetTypeName(string objectPathId, XmlDocument doc, XmlNamespaceManager nsManager, CsomTypesDefinition typesDefinition)
36+
{
37+
var objectPath = doc.SelectSingleNode($"//ns:ObjectPaths/*[@Id='{objectPathId}']", nsManager);
38+
if (objectPath == null)
39+
{
40+
return null;
41+
}
42+
43+
if (objectPath.Name == "Constructor" ||
44+
objectPath.Name == "StaticProperty")
45+
{
46+
var typeIdAttr = objectPath.Attributes?["TypeId"];
47+
if (typeIdAttr != null)
48+
{
49+
var typeId = typeIdAttr.Value.Trim('{', '}');
50+
if (typesDefinition?.Types?.TryGetValue(typeId, out string? typeName) == true)
51+
{
52+
if (objectPath.Name == "StaticProperty")
53+
{
54+
var nameAttr = objectPath.Attributes?["Name"];
55+
if (nameAttr != null)
56+
{
57+
return $"{typeName}.{nameAttr.Value}";
58+
}
59+
}
60+
return typeName;
61+
}
62+
else
63+
{
64+
return null;
65+
}
66+
}
67+
return null;
68+
}
69+
70+
var parentIdAttr = objectPath.Attributes?["ParentId"];
71+
if (parentIdAttr == null)
72+
{
73+
return null;
74+
}
75+
var parentId = parentIdAttr.Value;
76+
77+
return GetTypeName(parentId, doc, nsManager, typesDefinition);
78+
}
79+
80+
private static string? GetObjectPathName(string objectPathId, ActionType actionType, XmlDocument doc, XmlNamespaceManager nsManager, CsomTypesDefinition typesDefinition)
81+
{
82+
var objectPath = doc.SelectSingleNode($"//ns:ObjectPaths/*[@Id='{objectPathId}']", nsManager);
83+
if (objectPath == null)
84+
{
85+
return null;
86+
}
87+
88+
var typeName = GetTypeName(objectPathId, doc, nsManager, typesDefinition);
89+
if (typeName == null)
90+
{
91+
return null;
92+
}
93+
94+
if (objectPath.Name == "Constructor")
95+
{
96+
var suffix = actionType == ActionType.Query ? "query" : "ctor";
97+
return $"{typeName}.{suffix}";
98+
}
99+
100+
if (objectPath.Name == "Method")
101+
{
102+
var nameAttr = objectPath.Attributes?["Name"];
103+
if (nameAttr == null)
104+
{
105+
return null;
106+
}
107+
var methodName = actionType == ActionType.Query ? "query" : nameAttr.Value;
108+
return $"{typeName}.{methodName}";
109+
}
110+
111+
if (objectPath.Name == "Property")
112+
{
113+
var nameAttr = objectPath.Attributes?["Name"];
114+
if (nameAttr == null)
115+
{
116+
return null;
117+
}
118+
if (typesDefinition?.ReturnTypes?.TryGetValue($"{typeName}.{nameAttr.Value}", out string? returnType) == true)
119+
{
120+
var methodName = actionType == ActionType.SetProperty ? "setProperty" : nameAttr.Value;
121+
return $"{returnType}.{methodName}";
122+
}
123+
else
124+
{
125+
return $"{typeName}.{nameAttr.Value}";
126+
}
127+
}
128+
129+
return null;
130+
}
131+
132+
private static ActionType GetActionType(string actionName)
133+
{
134+
return actionName switch
135+
{
136+
"ObjectPath" => ActionType.ObjectPath,
137+
"Query" => ActionType.Query,
138+
"SetProperty" => ActionType.SetProperty,
139+
_ => throw new ArgumentOutOfRangeException(nameof(actionName), $"Unknown action type: {actionName}")
140+
};
141+
}
142+
143+
public static (IEnumerable<string> Actions, IEnumerable<string> Errors) GetActions(string xml, CsomTypesDefinition typesDefinition)
144+
{
145+
if (typesDefinition?.Types == null || string.IsNullOrEmpty(xml))
146+
{
147+
return ([], []);
148+
}
149+
150+
var actions = new List<string>();
151+
var errors = new List<string>();
152+
153+
try
154+
{
155+
// Load the XML
156+
var doc = new XmlDocument();
157+
doc.LoadXml(xml);
158+
159+
var nsManager = new XmlNamespaceManager(doc.NameTable);
160+
var defaultNamespace = doc.DocumentElement?.NamespaceURI ?? string.Empty;
161+
if (!string.IsNullOrEmpty(defaultNamespace))
162+
{
163+
nsManager.AddNamespace("ns", defaultNamespace);
164+
}
165+
166+
// Get the Actions element
167+
var actionsNode = doc.SelectSingleNode("//ns:Actions", nsManager);
168+
if (actionsNode == null)
169+
{
170+
errors.Add("Actions node not found in XML.");
171+
// If Actions node is not found, return empty list
172+
return (actions, errors);
173+
}
174+
175+
// Process all child Action elements
176+
foreach (XmlNode actionNode in actionsNode.ChildNodes)
177+
{
178+
var actionType = GetActionType(actionNode.Name);
179+
180+
// Extract ObjectPathId attribute
181+
var objectPathIdAttr = actionNode.Attributes?["ObjectPathId"];
182+
if (objectPathIdAttr == null)
183+
{
184+
errors.Add($"ObjectPathId attribute not found for action: {actionNode.OuterXml}");
185+
continue;
186+
}
187+
188+
var objectPathId = objectPathIdAttr.Value;
189+
190+
var type = GetObjectPathName(objectPathId, actionType, doc, nsManager, typesDefinition);
191+
192+
if (type != null)
193+
{
194+
actions.Add(type);
195+
}
196+
}
197+
}
198+
catch (Exception ex)
199+
{
200+
Console.WriteLine($"Error parsing XML: {ex.Message}");
201+
}
202+
203+
return (actions, errors);
204+
}
205+
206+
public static (string[] MinimalScopes, string[] UnmatchedOperations) GetMinimalScopes(IEnumerable<string> actions, AccessType accessType, CsomTypesDefinition typesDefinition)
207+
{
208+
var operationsAndScopes = typesDefinition?.Actions
209+
?.Where(o => o.Value.Delegated != null || o.Value.Application != null)
210+
.ToDictionary(
211+
o => o.Key,
212+
o => accessType == AccessType.Delegated ? o.Value.Delegated : o.Value.Application
213+
);
214+
return MinimalPermissionsUtils.GetMinimalScopes([.. actions], operationsAndScopes!);
215+
}
216+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System.Text.Json.Serialization;
6+
7+
namespace DevProxy.Plugins.MinimalPermissions;
8+
9+
public class CsomTypesDefinition
10+
{
11+
[JsonPropertyName("$schema")]
12+
public string? Schema { get; set; }
13+
14+
[JsonPropertyName("types")]
15+
public Dictionary<string, string>? Types { get; set; }
16+
17+
[JsonPropertyName("actions")]
18+
public Dictionary<string, CsomActionPermissions>? Actions { get; set; }
19+
[JsonPropertyName("returnTypes")]
20+
public Dictionary<string, string>? ReturnTypes { get; set; }
21+
}
22+
23+
public class CsomActionPermissions
24+
{
25+
[JsonPropertyName("delegated")]
26+
public string[]? Delegated { get; set; }
27+
28+
[JsonPropertyName("application")]
29+
public string[]? Application { get; set; }
30+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using DevProxy.Abstractions;
6+
using DevProxy.Plugins.MinimalPermissions;
7+
using Microsoft.Extensions.Logging;
8+
using System.Text.Json;
9+
10+
namespace DevProxy.Plugins.RequestLogs;
11+
12+
internal class CsomTypesDefinitionLoader(ILogger logger, MinimalCsomPermissionsPluginConfiguration configuration, bool validateSchemas) : BaseLoader(logger, validateSchemas)
13+
{
14+
private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger));
15+
private readonly MinimalCsomPermissionsPluginConfiguration _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
16+
protected override string FilePath => Path.Combine(Directory.GetCurrentDirectory(), _configuration.TypesFilePath!);
17+
18+
protected override void LoadData(string fileContents)
19+
{
20+
try
21+
{
22+
var types = JsonSerializer.Deserialize<CsomTypesDefinition>(fileContents, ProxyUtils.JsonSerializerOptions);
23+
if (types is not null)
24+
{
25+
_configuration.TypesDefinitions = types;
26+
_logger.LogInformation("CSOM types definitions loaded from {File}", _configuration.TypesFilePath);
27+
}
28+
else
29+
{
30+
_configuration.TypesDefinitions = new CsomTypesDefinition();
31+
}
32+
}
33+
catch (Exception ex)
34+
{
35+
_logger.LogError(ex, "An error has occurred while reading {configurationFile}:", _configuration.TypesFilePath);
36+
}
37+
}
38+
}

0 commit comments

Comments
 (0)