108 lines
5.0 KiB
C#
108 lines
5.0 KiB
C#
using Microsoft.Graph;
|
|
using Microsoft.Graph.Models;
|
|
using Microsoft.SharePoint.Client;
|
|
using Serilog;
|
|
using SharepointToolbox.Web.Core.Helpers;
|
|
using SharepointToolbox.Web.Core.Models;
|
|
using SharepointToolbox.Web.Services.Audit;
|
|
using AppGraphClientFactory = SharepointToolbox.Web.Infrastructure.Auth.GraphClientFactory;
|
|
|
|
namespace SharepointToolbox.Web.Services;
|
|
|
|
public class BulkMemberService : IBulkMemberService
|
|
{
|
|
private readonly AppGraphClientFactory _graphClientFactory;
|
|
private readonly IAuditService _audit;
|
|
|
|
public BulkMemberService(AppGraphClientFactory graphClientFactory, IAuditService audit)
|
|
{
|
|
_graphClientFactory = graphClientFactory;
|
|
_audit = audit;
|
|
}
|
|
|
|
public async Task<BulkOperationSummary<BulkMemberRow>> AddMembersAsync(
|
|
ClientContext ctx, TenantProfile profile,
|
|
IReadOnlyList<BulkMemberRow> rows,
|
|
IProgress<OperationProgress> progress, CancellationToken ct)
|
|
{
|
|
var result = await BulkOperationRunner.RunAsync(rows,
|
|
async (row, idx, token) => await AddSingleMemberAsync(ctx, profile, row, progress, token),
|
|
progress, ct);
|
|
var sites = rows.Select(r => r.GroupUrl ?? ctx.Url).Distinct().ToList();
|
|
await _audit.LogAsync("BulkAddMembers", profile.Name, sites, $"{result.SuccessCount} succeeded, {(result.TotalCount - result.SuccessCount)} failed");
|
|
return result;
|
|
}
|
|
|
|
private async Task AddSingleMemberAsync(
|
|
ClientContext ctx, TenantProfile profile, BulkMemberRow row,
|
|
IProgress<OperationProgress> progress, CancellationToken ct)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(row.GroupUrl))
|
|
{
|
|
await AddToClassicGroupAsync(ctx, row.GroupName, row.Email, row.Role, progress, ct);
|
|
return;
|
|
}
|
|
try
|
|
{
|
|
var graphClient = await _graphClientFactory.CreateClientAsync(profile);
|
|
var groupId = await ResolveGroupIdAsync(graphClient, row.GroupUrl, ct);
|
|
if (groupId != null)
|
|
{
|
|
await AddViaGraphAsync(graphClient, groupId, row.Email, row.Role, ct);
|
|
Log.Information("Added {Email} to M365 group {Group} via Graph", row.Email, row.GroupName);
|
|
return;
|
|
}
|
|
}
|
|
catch (OperationCanceledException) { throw; }
|
|
catch (Exception ex) { Log.Warning("Graph API failed for {Url}, falling back to CSOM: {Error}", row.GroupUrl, ex.Message); }
|
|
await AddToClassicGroupAsync(ctx, row.GroupName, row.Email, row.Role, progress, ct);
|
|
}
|
|
|
|
private static async Task AddViaGraphAsync(GraphServiceClient graphClient, string groupId, string email, string role, CancellationToken ct)
|
|
{
|
|
var user = await graphClient.Users[email].GetAsync(cancellationToken: ct);
|
|
if (user?.Id == null) throw new InvalidOperationException($"User not found: {email}");
|
|
var userRef = $"https://graph.microsoft.com/v1.0/directoryObjects/{user.Id}";
|
|
var body = new ReferenceCreate { OdataId = userRef };
|
|
if (role.Equals("Owner", StringComparison.OrdinalIgnoreCase))
|
|
await graphClient.Groups[groupId].Owners.Ref.PostAsync(body, cancellationToken: ct);
|
|
else
|
|
await graphClient.Groups[groupId].Members.Ref.PostAsync(body, cancellationToken: ct);
|
|
}
|
|
|
|
private static async Task<string?> ResolveGroupIdAsync(GraphServiceClient graphClient, string siteUrl, CancellationToken ct)
|
|
{
|
|
try
|
|
{
|
|
var uri = new Uri(siteUrl);
|
|
var site = await graphClient.Sites[$"{uri.Host}:{uri.AbsolutePath.TrimEnd('/')}"].GetAsync(cancellationToken: ct);
|
|
if (site?.Id == null) return null;
|
|
var groups = await graphClient.Groups.GetAsync(r =>
|
|
{
|
|
r.QueryParameters.Filter = "resourceProvisioningOptions/any(x:x eq 'Team')";
|
|
r.QueryParameters.Select = new[] { "id" };
|
|
}, cancellationToken: ct);
|
|
return groups?.Value?.FirstOrDefault()?.Id;
|
|
}
|
|
catch (OperationCanceledException) { throw; }
|
|
catch (Exception ex) { Log.Debug("Group resolve failed for {Url}: {Error}", siteUrl, ex.Message); return null; }
|
|
}
|
|
|
|
private static async Task AddToClassicGroupAsync(
|
|
ClientContext ctx, string groupName, string email, string role,
|
|
IProgress<OperationProgress> progress, CancellationToken ct)
|
|
{
|
|
ctx.Load(ctx.Web.SiteGroups);
|
|
await ExecuteQueryRetryHelper.ExecuteQueryRetryAsync(ctx, progress, ct);
|
|
Microsoft.SharePoint.Client.Group? targetGroup = null;
|
|
foreach (var group in ctx.Web.SiteGroups)
|
|
if (group.Title.Equals(groupName, StringComparison.OrdinalIgnoreCase)) { targetGroup = group; break; }
|
|
if (targetGroup == null) throw new InvalidOperationException($"SharePoint group not found: {groupName}");
|
|
var user = ctx.Web.EnsureUser(email);
|
|
ctx.Load(user);
|
|
await ExecuteQueryRetryHelper.ExecuteQueryRetryAsync(ctx, progress, ct);
|
|
targetGroup.Users.AddUser(user);
|
|
await ExecuteQueryRetryHelper.ExecuteQueryRetryAsync(ctx, progress, ct);
|
|
}
|
|
}
|