feat(17-01): ResolvedMember model, ISharePointGroupResolver interface, SharePointGroupResolver CSOM+Graph implementation

- ResolvedMember record in Core/Models with DisplayName and Login
- ISharePointGroupResolver interface with ResolveGroupsAsync contract
- SharePointGroupResolver: CSOM group user loading + Graph transitive AAD resolution
- Internal static helpers IsAadGroup, ExtractAadGroupId, StripClaims (all green unit tests)
- Graceful error handling: exceptions return empty list per group, never throw
- OrdinalIgnoreCase result dict; lazy Graph client creation on first AAD group
This commit is contained in:
Dev
2026-04-09 13:04:56 +02:00
parent 0f8b1953e1
commit 543b863283
4 changed files with 217 additions and 7 deletions

View File

@@ -0,0 +1,37 @@
using Microsoft.SharePoint.Client;
using SharepointToolbox.Core.Models;
namespace SharepointToolbox.Services;
/// <summary>
/// Resolves SharePoint group names to their transitive member lists via CSOM + Microsoft Graph.
/// Used as a pre-render step in HTML report export (Phase 17).
/// </summary>
public interface ISharePointGroupResolver
{
/// <summary>
/// Resolves the given SharePoint group names to their transitive leaf-user members.
///
/// For each group name:
/// - CSOM loads the direct members from <see cref="Microsoft.SharePoint.Client.GroupCollection"/>.
/// - Any members that are nested AAD groups (login prefix <c>c:0t.c|tenant|</c>) are expanded
/// transitively via the Microsoft Graph <c>groups/{id}/transitiveMembers/microsoft.graph.user</c> endpoint.
/// - Members are de-duplicated by login (OrdinalIgnoreCase).
///
/// Failures (throttling, group not found, Graph errors) are handled gracefully:
/// the group entry is included in the result with an empty member list rather than throwing.
/// </summary>
/// <param name="ctx">SharePoint ClientContext for CSOM group member loading.</param>
/// <param name="clientId">App registration client ID used by <see cref="SharepointToolbox.Infrastructure.Auth.GraphClientFactory"/>.</param>
/// <param name="groupNames">List of SharePoint group display names to resolve.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>
/// A read-only dictionary keyed by group name (OrdinalIgnoreCase) mapping to the list of resolved members.
/// Never throws — a group with a resolution failure maps to an empty list.
/// </returns>
Task<IReadOnlyDictionary<string, IReadOnlyList<ResolvedMember>>> ResolveGroupsAsync(
ClientContext ctx,
string clientId,
IReadOnlyList<string> groupNames,
CancellationToken ct);
}