Initial commit
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
using Microsoft.Graph;
|
||||
using Microsoft.Graph.Models;
|
||||
using Microsoft.SharePoint.Client;
|
||||
using Serilog;
|
||||
using SharepointToolbox.Web.Core.Helpers;
|
||||
using SharepointToolbox.Web.Core.Models;
|
||||
using AppGraphClientFactory = SharepointToolbox.Web.Infrastructure.Auth.GraphClientFactory;
|
||||
using GraphUser = Microsoft.Graph.Models.User;
|
||||
using GraphUserCollectionResponse = Microsoft.Graph.Models.UserCollectionResponse;
|
||||
|
||||
namespace SharepointToolbox.Web.Services;
|
||||
|
||||
public class SharePointGroupResolver : ISharePointGroupResolver
|
||||
{
|
||||
private readonly AppGraphClientFactory _graphClientFactory;
|
||||
|
||||
public SharePointGroupResolver(AppGraphClientFactory graphClientFactory)
|
||||
{
|
||||
_graphClientFactory = graphClientFactory;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyDictionary<string, IReadOnlyList<ResolvedMember>>> ResolveGroupsAsync(
|
||||
ClientContext ctx,
|
||||
TenantProfile profile,
|
||||
IReadOnlyList<string> groupNames,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var result = new Dictionary<string, IReadOnlyList<ResolvedMember>>(StringComparer.OrdinalIgnoreCase);
|
||||
if (groupNames.Count == 0) return result;
|
||||
|
||||
GraphServiceClient? graphClient = null;
|
||||
|
||||
var groupTitles = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
try
|
||||
{
|
||||
ctx.Load(ctx.Web.SiteGroups, gs => gs.Include(g => g.Title));
|
||||
await ExecuteQueryRetryHelper.ExecuteQueryRetryAsync(ctx, null, ct);
|
||||
foreach (var g in ctx.Web.SiteGroups) groupTitles.Add(g.Title);
|
||||
}
|
||||
catch (OperationCanceledException) { throw; }
|
||||
catch (Exception ex) { Log.Warning("Could not enumerate SiteGroups on {Url}: {Error}", ctx.Url, ex.Message); }
|
||||
|
||||
foreach (var groupName in groupNames.Distinct(StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
if (!groupTitles.Contains(groupName))
|
||||
{
|
||||
Log.Debug("SP group '{Group}' not present on {Url}; skipping.", groupName, ctx.Url);
|
||||
result[groupName] = Array.Empty<ResolvedMember>();
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var group = ctx.Web.SiteGroups.GetByName(groupName);
|
||||
ctx.Load(group.Users, users => users.Include(u => u.Title, u => u.LoginName, u => u.PrincipalType));
|
||||
await ExecuteQueryRetryHelper.ExecuteQueryRetryAsync(ctx, null, ct);
|
||||
|
||||
var members = new List<ResolvedMember>();
|
||||
foreach (var user in group.Users)
|
||||
{
|
||||
if (IsAadGroup(user.LoginName))
|
||||
{
|
||||
graphClient ??= await _graphClientFactory.CreateClientAsync(profile);
|
||||
var aadId = ExtractAadGroupId(user.LoginName);
|
||||
var leafUsers = await ResolveAadGroupAsync(graphClient, aadId, ct);
|
||||
members.AddRange(leafUsers);
|
||||
}
|
||||
else
|
||||
{
|
||||
members.Add(new ResolvedMember(user.Title ?? user.LoginName, StripClaims(user.LoginName)));
|
||||
}
|
||||
}
|
||||
result[groupName] = members.DistinctBy(m => m.Login, StringComparer.OrdinalIgnoreCase).ToList();
|
||||
}
|
||||
catch (OperationCanceledException) { throw; }
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning("Could not resolve SP group '{Group}': {Error}", groupName, ex.Message);
|
||||
result[groupName] = Array.Empty<ResolvedMember>();
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
internal static bool IsAadGroup(string login) =>
|
||||
login.StartsWith("c:0t.c|", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
internal static string ExtractAadGroupId(string login) => login[(login.LastIndexOf('|') + 1)..];
|
||||
internal static string StripClaims(string login) => login[(login.LastIndexOf('|') + 1)..];
|
||||
|
||||
private static async Task<IEnumerable<ResolvedMember>> ResolveAadGroupAsync(
|
||||
GraphServiceClient graphClient, string aadGroupId, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await graphClient.Groups[aadGroupId].TransitiveMembers.GraphUser.GetAsync(config =>
|
||||
{
|
||||
config.QueryParameters.Select = new[] { "displayName", "userPrincipalName" };
|
||||
config.QueryParameters.Top = 999;
|
||||
}, ct);
|
||||
if (response?.Value is null) return Enumerable.Empty<ResolvedMember>();
|
||||
|
||||
var members = new List<ResolvedMember>();
|
||||
var iter = PageIterator<GraphUser, GraphUserCollectionResponse>.CreatePageIterator(
|
||||
graphClient, response,
|
||||
user =>
|
||||
{
|
||||
if (ct.IsCancellationRequested) return false;
|
||||
members.Add(new ResolvedMember(
|
||||
user.DisplayName ?? user.UserPrincipalName ?? "Unknown",
|
||||
user.UserPrincipalName ?? string.Empty));
|
||||
return true;
|
||||
});
|
||||
await iter.IterateAsync(ct);
|
||||
return members;
|
||||
}
|
||||
catch (OperationCanceledException) { throw; }
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning("Could not resolve AAD group '{Id}' transitively: {Error}", aadGroupId, ex.Message);
|
||||
return Enumerable.Empty<ResolvedMember>();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user