using SharepointToolbox.Web.Core.Helpers; using SharepointToolbox.Web.Core.Models; namespace SharepointToolbox.Web.Services; 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, TenantProfile currentProfile, IReadOnlyList targetUserLogins, IReadOnlyList sites, ScanOptions options, IProgress progress, CancellationToken ct, Func>? onAccessDenied = null) { 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, TenantId = currentProfile.TenantId, ClientId = currentProfile.ClientId, Name = site.Title }; var ctx = await sessionManager.GetOrCreateContextAsync(profile, ct); IReadOnlyList permEntries; try { permEntries = await _permissionsService.ScanSiteAsync(ctx, options, progress, ct); } catch (Microsoft.SharePoint.Client.ServerUnauthorizedAccessException) when (onAccessDenied != null) { var elevated = await onAccessDenied(site.Url, ct); if (!elevated) throw; var retryCtx = await sessionManager.GetOrCreateContextAsync(profile, ct); permEntries = await _permissionsService.ScanSiteAsync(retryCtx, options, progress, ct); } allEntries.AddRange(TransformEntries(permEntries, targets, site)); } progress.Report(new OperationProgress(sites.Count, sites.Count, $"Audit complete: {allEntries.Count} access entries found.")); return allEntries; } private static IEnumerable TransformEntries( IReadOnlyList permEntries, HashSet targets, SiteInfo site) { foreach (var entry in permEntries) { var logins = entry.UserLogins.Split(';', StringSplitOptions.RemoveEmptyEntries); var names = entry.Users.Split(';', StringSplitOptions.RemoveEmptyEntries); 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; bool isTarget = targets.Any(t => loginLower.Contains(t) || t.Contains(loginLower)); if (!isTarget) continue; var accessType = !entry.HasUniquePermissions ? AccessType.Inherited : entry.GrantedThrough.StartsWith("SharePoint Group:", StringComparison.OrdinalIgnoreCase) ? AccessType.Group : AccessType.Direct; foreach (var level in permLevels) { var trimmed = level.Trim(); if (string.IsNullOrEmpty(trimmed)) continue; yield return new UserAccessEntry( displayName, StripClaimsPrefix(login), site.Url, site.Title, entry.ObjectType, entry.Title, entry.Url, trimmed, accessType, entry.GrantedThrough, HighPrivilegeLevels.Contains(trimmed), PermissionEntryHelper.IsExternalUser(login), entry.TargetUrl, entry.TargetLabel, entry.SharingLinkType); } } } } private static string StripClaimsPrefix(string login) { int pipe = login.LastIndexOf('|'); return pipe >= 0 ? login[(pipe + 1)..] : login; } }