using SharepointToolbox.Core.Helpers; using SharepointToolbox.Core.Models; namespace SharepointToolbox.Services; /// /// Scans permissions across multiple sites via PermissionsService, /// then filters and transforms results into user-centric UserAccessEntry records. /// public class UserAccessAuditService : IUserAccessAuditService { private readonly IPermissionsService _permissionsService; private static readonly HashSet HighPrivilegeLevels = new(StringComparer.OrdinalIgnoreCase) { "Full Control", "Site Collection Administrator" }; public UserAccessAuditService(IPermissionsService permissionsService) { _permissionsService = permissionsService; } public async Task> AuditUsersAsync( ISessionManager sessionManager, IReadOnlyList targetUserLogins, IReadOnlyList sites, ScanOptions options, IProgress progress, CancellationToken ct) { // Normalize target logins for case-insensitive matching. // Users may be identified by email ("alice@contoso.com") or full claim // ("i:0#.f|membership|alice@contoso.com"), so we match on "contains". var targets = targetUserLogins .Select(l => l.Trim().ToLowerInvariant()) .Where(l => l.Length > 0) .ToHashSet(); if (targets.Count == 0) return Array.Empty(); var allEntries = new List(); for (int i = 0; i < sites.Count; i++) { ct.ThrowIfCancellationRequested(); var site = sites[i]; progress.Report(new OperationProgress(i, sites.Count, $"Scanning site {i + 1}/{sites.Count}: {site.Title}...")); var profile = new TenantProfile { TenantUrl = site.Url, ClientId = string.Empty, // Will be set by SessionManager from cached session Name = site.Title }; var ctx = await sessionManager.GetOrCreateContextAsync(profile, ct); var permEntries = await _permissionsService.ScanSiteAsync(ctx, options, progress, ct); var userEntries = TransformEntries(permEntries, targets, site); allEntries.AddRange(userEntries); } progress.Report(new OperationProgress(sites.Count, sites.Count, $"Audit complete: {allEntries.Count} access entries found.")); return allEntries; } /// /// Transforms PermissionEntry list into UserAccessEntry list, /// filtering to only entries that match target user logins. /// private static IEnumerable TransformEntries( IReadOnlyList permEntries, HashSet targets, SiteInfo site) { foreach (var entry in permEntries) { // Split semicolon-delimited Users and UserLogins var logins = entry.UserLogins.Split(';', StringSplitOptions.RemoveEmptyEntries); var names = entry.Users.Split(';', StringSplitOptions.RemoveEmptyEntries); // Split semicolon-delimited PermissionLevels var permLevels = entry.PermissionLevels.Split(';', StringSplitOptions.RemoveEmptyEntries); for (int u = 0; u < logins.Length; u++) { var login = logins[u].Trim(); var loginLower = login.ToLowerInvariant(); var displayName = u < names.Length ? names[u].Trim() : login; // Check if this login matches any target user. // Match by "contains" because SharePoint claims may wrap the email: // "i:0#.f|membership|alice@contoso.com" contains "alice@contoso.com" bool isTarget = targets.Any(t => loginLower.Contains(t) || t.Contains(loginLower)); if (!isTarget) continue; // Determine access type var accessType = ClassifyAccessType(entry); // Emit one UserAccessEntry per permission level foreach (var level in permLevels) { var trimmedLevel = level.Trim(); if (string.IsNullOrEmpty(trimmedLevel)) continue; yield return new UserAccessEntry( UserDisplayName: displayName, UserLogin: login, SiteUrl: site.Url, SiteTitle: site.Title, ObjectType: entry.ObjectType, ObjectTitle: entry.Title, ObjectUrl: entry.Url, PermissionLevel: trimmedLevel, AccessType: accessType, GrantedThrough: entry.GrantedThrough, IsHighPrivilege: HighPrivilegeLevels.Contains(trimmedLevel), IsExternalUser: PermissionEntryHelper.IsExternalUser(login)); } } } } /// /// Classifies a PermissionEntry into Direct, Group, or Inherited access type. /// private static AccessType ClassifyAccessType(PermissionEntry entry) { // Inherited: object does not have unique permissions if (!entry.HasUniquePermissions) return AccessType.Inherited; // Group: GrantedThrough starts with "SharePoint Group:" if (entry.GrantedThrough.StartsWith("SharePoint Group:", StringComparison.OrdinalIgnoreCase)) return AccessType.Group; // Direct: unique permissions, granted directly return AccessType.Direct; } }