Skip to content

Adds Minimal SharePoint CSOM permissions plugin. Closes #1018 #1061

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
216 changes: 216 additions & 0 deletions dev-proxy-plugins/MinimalPermissions/CsomParser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System.Text.Json;
using System.Text.Json.Serialization;
using System.Xml;

namespace DevProxy.Plugins.MinimalPermissions;

enum ActionType
{
ObjectPath,
Query,
SetProperty
}

enum AccessType
{
Delegated,
Application
}

static class CsomParser
{
private static readonly JsonSerializerOptions options = new()
{
PropertyNameCaseInsensitive = true,
Converters =
{
new JsonStringEnumConverter(JsonNamingPolicy.CamelCase)
}
};

private static string? GetTypeName(string objectPathId, XmlDocument doc, XmlNamespaceManager nsManager, CsomTypesDefinition typesDefinition)
{
var objectPath = doc.SelectSingleNode($"//ns:ObjectPaths/*[@Id='{objectPathId}']", nsManager);
if (objectPath == null)
{
return null;
}

if (objectPath.Name == "Constructor" ||
objectPath.Name == "StaticProperty")
{
var typeIdAttr = objectPath.Attributes?["TypeId"];
if (typeIdAttr != null)
{
var typeId = typeIdAttr.Value.Trim('{', '}');
if (typesDefinition?.Types?.TryGetValue(typeId, out string? typeName) == true)
{
if (objectPath.Name == "StaticProperty")
{
var nameAttr = objectPath.Attributes?["Name"];
if (nameAttr != null)
{
return $"{typeName}.{nameAttr.Value}";
}
}
return typeName;
}
else
{
return null;
}
}
return null;
}

var parentIdAttr = objectPath.Attributes?["ParentId"];
if (parentIdAttr == null)
{
return null;
}
var parentId = parentIdAttr.Value;

return GetTypeName(parentId, doc, nsManager, typesDefinition);
}

private static string? GetObjectPathName(string objectPathId, ActionType actionType, XmlDocument doc, XmlNamespaceManager nsManager, CsomTypesDefinition typesDefinition)
{
var objectPath = doc.SelectSingleNode($"//ns:ObjectPaths/*[@Id='{objectPathId}']", nsManager);
if (objectPath == null)
{
return null;
}

var typeName = GetTypeName(objectPathId, doc, nsManager, typesDefinition);
if (typeName == null)
{
return null;
}

if (objectPath.Name == "Constructor")
{
var suffix = actionType == ActionType.Query ? "query" : "ctor";
return $"{typeName}.{suffix}";
}

if (objectPath.Name == "Method")
{
var nameAttr = objectPath.Attributes?["Name"];
if (nameAttr == null)
{
return null;
}
var methodName = actionType == ActionType.Query ? "query" : nameAttr.Value;
return $"{typeName}.{methodName}";
}

if (objectPath.Name == "Property")
{
var nameAttr = objectPath.Attributes?["Name"];
if (nameAttr == null)
{
return null;
}
if (typesDefinition?.ReturnTypes?.TryGetValue($"{typeName}.{nameAttr.Value}", out string? returnType) == true)
{
var methodName = actionType == ActionType.SetProperty ? "setProperty" : nameAttr.Value;
return $"{returnType}.{methodName}";
}
else
{
return $"{typeName}.{nameAttr.Value}";
}
}

return null;
}

private static ActionType GetActionType(string actionName)
{
return actionName switch
{
"ObjectPath" => ActionType.ObjectPath,
"Query" => ActionType.Query,
"SetProperty" => ActionType.SetProperty,
_ => throw new ArgumentOutOfRangeException(nameof(actionName), $"Unknown action type: {actionName}")
};
}

public static (IEnumerable<string> Actions, IEnumerable<string> Errors) GetActions(string xml, CsomTypesDefinition typesDefinition)
{
if (typesDefinition?.Types == null || string.IsNullOrEmpty(xml))
{
return ([], []);
}

var actions = new List<string>();
var errors = new List<string>();

try
{
// Load the XML
var doc = new XmlDocument();
doc.LoadXml(xml);

var nsManager = new XmlNamespaceManager(doc.NameTable);
var defaultNamespace = doc.DocumentElement?.NamespaceURI ?? string.Empty;
if (!string.IsNullOrEmpty(defaultNamespace))
{
nsManager.AddNamespace("ns", defaultNamespace);
}

// Get the Actions element
var actionsNode = doc.SelectSingleNode("//ns:Actions", nsManager);
if (actionsNode == null)
{
errors.Add("Actions node not found in XML.");
// If Actions node is not found, return empty list
return (actions, errors);
}

// Process all child Action elements
foreach (XmlNode actionNode in actionsNode.ChildNodes)
{
var actionType = GetActionType(actionNode.Name);

// Extract ObjectPathId attribute
var objectPathIdAttr = actionNode.Attributes?["ObjectPathId"];
if (objectPathIdAttr == null)
{
errors.Add($"ObjectPathId attribute not found for action: {actionNode.OuterXml}");
continue;
}

var objectPathId = objectPathIdAttr.Value;

var type = GetObjectPathName(objectPathId, actionType, doc, nsManager, typesDefinition);

if (type != null)
{
actions.Add(type);
}
}
}
catch (Exception ex)
{
Console.WriteLine($"Error parsing XML: {ex.Message}");
}

return (actions, errors);
}

public static (string[] MinimalScopes, string[] UnmatchedOperations) GetMinimalScopes(IEnumerable<string> actions, AccessType accessType, CsomTypesDefinition typesDefinition)
{
var operationsAndScopes = typesDefinition?.Actions
?.Where(o => o.Value.Delegated != null || o.Value.Application != null)
.ToDictionary(
o => o.Key,
o => accessType == AccessType.Delegated ? o.Value.Delegated : o.Value.Application
);
return MinimalPermissionsUtils.GetMinimalScopes([.. actions], operationsAndScopes!);
}
}
30 changes: 30 additions & 0 deletions dev-proxy-plugins/MinimalPermissions/CsomTypesDefinition.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System.Text.Json.Serialization;

namespace DevProxy.Plugins.MinimalPermissions;

public class CsomTypesDefinition
{
[JsonPropertyName("$schema")]
public string? Schema { get; set; }

[JsonPropertyName("types")]
public Dictionary<string, string>? Types { get; set; }

[JsonPropertyName("actions")]
public Dictionary<string, CsomActionPermissions>? Actions { get; set; }
[JsonPropertyName("returnTypes")]
public Dictionary<string, string>? ReturnTypes { get; set; }
}

public class CsomActionPermissions
{
[JsonPropertyName("delegated")]
public string[]? Delegated { get; set; }

[JsonPropertyName("application")]
public string[]? Application { get; set; }
}
38 changes: 38 additions & 0 deletions dev-proxy-plugins/RequestLogs/CsomTypesDefinitionLoader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using DevProxy.Abstractions;
using DevProxy.Plugins.MinimalPermissions;
using Microsoft.Extensions.Logging;
using System.Text.Json;

namespace DevProxy.Plugins.RequestLogs;

internal class CsomTypesDefinitionLoader(ILogger logger, MinimalCsomPermissionsPluginConfiguration configuration, bool validateSchemas) : BaseLoader(logger, validateSchemas)
{
private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger));
private readonly MinimalCsomPermissionsPluginConfiguration _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
protected override string FilePath => Path.Combine(Directory.GetCurrentDirectory(), _configuration.TypesFilePath!);

protected override void LoadData(string fileContents)
{
try
{
var types = JsonSerializer.Deserialize<CsomTypesDefinition>(fileContents, ProxyUtils.JsonSerializerOptions);
if (types is not null)
{
_configuration.TypesDefinitions = types;
_logger.LogInformation("CSOM types definitions loaded from {File}", _configuration.TypesFilePath);
}
else
{
_configuration.TypesDefinitions = new CsomTypesDefinition();
}
}
catch (Exception ex)
{
_logger.LogError(ex, "An error has occurred while reading {configurationFile}:", _configuration.TypesFilePath);
}
}
}
Loading