Initial commit
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user